290 lines
7.4 KiB
Lua
290 lines
7.4 KiB
Lua
-- 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 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+)')
|
|
end
|
|
end
|
|
|
|
if not fb_name then
|
|
return nil, "No FUNCTION_BLOCK or FUNCTION 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
|