-- FB/Function Parser for SCL -- Parses FUNCTION_BLOCK and FUNCTION definitions from .scl files -- Extracts VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT sections with parameter details local M = {} -- Cache for parsed FBs/Functions: { [fb_name] = fb_definition } local fb_cache = {} -- FB/Function Definition structure: -- { -- name = "LLT_CmMotorStarter_1", -- type = "FUNCTION_BLOCK", -- or "FUNCTION" -- filename = "path/to/file.scl", -- inputs = { -- { name = "enable", type = "Bool", comment = "TRUE: Enable...", has_default = false } -- }, -- outputs = { ... }, -- inouts = { ... } -- } -- Clear the FB cache function M.clear_cache() for k in pairs(fb_cache) do fb_cache[k] = nil end end -- Get cached FB/Function function M.get_fb(fb_name) return fb_cache[fb_name] end -- Check if a name is a known FB/Function type function M.is_fb_type(fb_name) if not fb_name then return false end return fb_cache[fb_name] ~= nil end -- Get FB interface (inputs, outputs, inouts) function M.get_fb_interface(fb_name) local fb = fb_cache[fb_name] if not fb then return nil end return { name = fb.name, type = fb.type, inputs = fb.inputs or {}, outputs = fb.outputs or {}, inouts = fb.inouts or {}, } end -- Parse attributes from a string like: { S7_SetPoint := 'True'; ExternalAccessible := 'False' } local function parse_attributes(attr_str) if not attr_str or attr_str == "" then return {} end local attrs = {} -- Match key := 'value' or key := value patterns for key, value in attr_str:gmatch("([%w_]+)%s*:=%s*'([^']+)'") do attrs[key] = value end for key, value in attr_str:gmatch("([%w_]+)%s*:=%s*(%w+)") do attrs[key] = value end return attrs end -- Parse a variable declaration line -- Returns: name, type, has_default, comment local function parse_var_declaration(line) -- Remove leading/trailing whitespace line = line:gsub("^%s*", ""):gsub("%s*$", "") -- Skip empty lines and pure comments if line == "" or line:match("^//") then return nil end -- Extract trailing comments (both // and (* *) style) local comment = line:match("//%s*(.+)$") or "" local decl_line = line:gsub("//.*$", "", 1):gsub("%s*$", "") -- Handle (* *) comments if decl_line:match("%(%*.-%*%)") then comment = decl_line:match("%(%*(.-)%*%)") or comment decl_line = decl_line:gsub("%(%*.-%*%)", ""):gsub("%s*$", "") end -- Pattern: name {attrs} : type [:= default]; -- Pattern: name : type [:= default]; local name, attrs_str, type_str, default_val = decl_line:match('^([%w_]+)%s*(%b{})%s*:%s*([^;]-)%s*:=%s*([^;]+);?$') if not name then -- Try without default value name, attrs_str, type_str = decl_line:match('^([%w_]+)%s*(%b{})%s*:%s*([^;]+);?$') end if not name then -- Try without attributes name, type_str, default_val = decl_line:match('^([%w_]+)%s*:%s*([^;]-)%s*:=%s*([^;]+);?$') end if not name then -- Simplest form: name : type; name, type_str = decl_line:match('^([%w_]+)%s*:%s*([^;]+);?$') end if not name or not type_str then return nil end -- Clean up type string type_str = type_str:gsub("^%s*", ""):gsub("%s*$", "") -- Check if type is a quoted FB/UDT reference local is_quoted = type_str:match('^".+"$') ~= nil local clean_type = type_str:gsub('"', '') return { name = name, type = clean_type, raw_type = type_str, has_default = default_val ~= nil, default_value = default_val, comment = comment, attributes = attrs_str and parse_attributes(attrs_str) or {}, is_quoted_type = is_quoted, } end -- Parse a VAR section (VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT, etc.) -- Returns array of variable definitions local function parse_var_section(content, section_name) local vars = {} -- Find the section - match from section_name to END_VAR local section_start = content:find(section_name, 1, true) if not section_start then return vars end local end_pos = content:find("END_VAR", section_start, true) if not end_pos then return vars end local section_content = content:sub(section_start + #section_name, end_pos - 1) -- Parse each line in the section for line in section_content:gmatch("[^\r\n]+") do local var = parse_var_declaration(line) if var then table.insert(vars, var) end end return vars end -- Parse FB/Function/OB content function M.parse_fb_content(content, filename) local fb = { filename = filename, inputs = {}, outputs = {}, inouts = {}, } -- Determine type and name local fb_type, fb_name -- Try FUNCTION_BLOCK fb_name = content:match('FUNCTION_BLOCK%s+"([^"]+)"') if fb_name then fb_type = "FUNCTION_BLOCK" else -- Try FUNCTION (with optional return type) fb_name = content:match('FUNCTION%s+"([^"]+)"') if fb_name then fb_type = "FUNCTION" -- Extract return type if present fb.return_type = content:match('FUNCTION%s+"[^"]+"%s*:%s*(%w+)') else -- Try ORGANIZATION_BLOCK fb_name = content:match('ORGANIZATION_BLOCK%s+"([^"]+)"') if fb_name then fb_type = "ORGANIZATION_BLOCK" end end end if not fb_name then return nil, "No FUNCTION_BLOCK, FUNCTION, or ORGANIZATION_BLOCK definition found" end fb.name = fb_name fb.type = fb_type -- Extract TITLE fb.title = content:match('TITLE%s*=%s*([^\n]+)') or "" fb.title = fb.title:gsub("^%s*", ""):gsub("%s*$", "") -- Extract VERSION fb.version = content:match('VERSION%s*:%s*([0-9.]+)') or "0.1" -- Parse VAR sections fb.inputs = parse_var_section(content, "VAR_INPUT") fb.outputs = parse_var_section(content, "VAR_OUTPUT") fb.inouts = parse_var_section(content, "VAR_IN_OUT") -- VAR section (static variables) - not typically needed for completion -- but could be useful for other features fb.vars = parse_var_section(content, "VAR%s") return fb end -- Parse a .scl file function M.parse_fb_file(filepath) local file = io.open(filepath, "r") if not file then return nil, "Cannot open file: " .. filepath end local content = file:read("*all") file:close() -- Check if this is an FB or Function file if not content:match("FUNCTION_BLOCK") and not content:match("FUNCTION") then return nil, "Not a FUNCTION_BLOCK or FUNCTION file" end local fb, err = M.parse_fb_content(content, filepath) if not fb then return nil, err end -- Cache the FB/Function fb_cache[fb.name] = fb return fb end -- Get all cached FB/Function names function M.get_all_fb_names() local names = {} for name, _ in pairs(fb_cache) do table.insert(names, name) end return names end -- Get count of cached FBs function M.get_cache_count() local count = 0 for _ in pairs(fb_cache) do count = count + 1 end return count end -- Debug: Print cache contents function M.debug_print_cache() print("=== FB/Function Cache Contents ===") print("Total FBs/Functions: " .. M.get_cache_count()) for name, fb in pairs(fb_cache) do print(string.format("%s: %s (v%s)", fb.type, name, fb.version)) print(string.format(" Title: %s", fb.title)) print(string.format(" File: %s", fb.filename or "unknown")) print(string.format(" Inputs: %d", #fb.inputs)) for _, input in ipairs(fb.inputs) do print(string.format(" → %s: %s", input.name, input.raw_type)) end print(string.format(" InOuts: %d", #fb.inouts)) for _, inout in ipairs(fb.inouts) do print(string.format(" ↔ %s: %s", inout.name, inout.raw_type)) end print(string.format(" Outputs: %d", #fb.outputs)) for _, output in ipairs(fb.outputs) do print(string.format(" ← %s: %s", output.name, output.raw_type)) end print("") end end return M