Files
lazar fb55915147 feat: major grammar upgrade for SCL spec conformance (Phase 1)
Comprehensive grammar rewrite to conform to SCL specs. Improves parse
success rate from 0% to 90% (187 of 208 reference project files).

Case-insensitive keywords:
- Add ci() helper for case-insensitive token matching
- All keywords (TYPE, STRUCT, VAR, IF, etc.) now match any case
- All type names (BOOL, Int, Dint, etc.) now match any case

New grammar constructs:
- RETURN statement
- GOTO statement
- AUTHOR/FAMILY/NAME block headers
- VAR DB_SPECIFIC section
- ARRAY[*] (variable-length arrays)
- Multidimensional ARRAY[a..b, c..d]
- REF_TO <type>
- VARIANT type
- Anonymous STRUCT...END_STRUCT as a type
- Compound assignment operators (+=, -=, *=, /=, &=, |=, ^=)
- Chained assignments (lvalue := lvalue := expression)
- Bit access (lvalue.%X0)
- Method calls (lvalue.field(...))
- Struct/array initializers ((TRUE, FALSE, ...), (name := value))
- END_STRUCT; (semicolon after END_STRUCT in UDTs)
- Typed DB associated block name (DATA_BLOCK "Name" : "UDT")
- BOM (Byte Order Mark) handling
- TITLE as single-token statement (avoids keyword/identifier conflict)

New type_builtin aliases:
- LWORD, LINT, ULINT, UDINT, DATE, TIME_OF_DAY, DATE_AND_TIME
- S5TIME, LTIME, DTL, LTOD, LDT, LTIME_OF_DAY, DATE_AND_LTIME

New literal formats:
- Binary (2#...), Octal (8#...)
- BOOL#TRUE/FALSE
- Typed integers (INT#, SINT#, DINT#, etc.)
- Date/time literals (DATE#, TOD#, DT#, LTIME#, S5TIME#, DTL#)
- WSTRING# and STRING# typed string literals
- C# char literal
- Time units with case-insensitive matching (T#1S, T#2s)

Updated constructs:
- CASE labels: comma-separated lists, string labels
- Function call parameters: interleaved input (:=) and output (=>) params
- FOR loop variable: accepts prefixed_identifier (#tempVar)
- Array bounds: accept string/identifier constants (Array[0.."MAX"])
- Author/Family values: accept unquoted text with hyphens/slashes

Known remaining issues (21 files, 10%):
- Triple-nested parentheses in expressions
- Some multi-line function call parameter patterns
- END_REGION with trailing region name
- Complex chained field access in assignments
2026-07-18 19:19:03 +02:00

676 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Case-insensitive token helper: matches any case combination of a word.
function ci(word) {
return token(new RegExp(word.split('').map(c =>
c.toUpperCase() === c.toLowerCase() ? c : '[' + c.toUpperCase() + c.toLowerCase() + ']'
).join('')));
}
module.exports = grammar({
name: 'scl',
extras: $ => [
/\s/,
$.line_comment,
$.block_comment,
$.c_style_comment
],
conflicts: $ => [
[$.case_item],
[$.region_statement],
[$.case_statement],
[$.fb_call, $.function_call],
[$.fb_parameter, $.parameter],
[$.var_declaration, $.var_temp_declaration],
[$.var_declaration, $.var_constant_declaration],
[$.var_declaration, $.var_retain_declaration],
[$.var_declaration, $.var_non_retain_declaration],
[$.var_declaration, $.var_db_specific_declaration],
[$.type, $.struct_type],
[$.expression, $.struct_initializer]
],
rules: {
source_file: $ => seq(
optional($.bom),
repeat(
choice(
$.organization_block,
$.function_block,
$.function,
$.type_definition,
$.data_block,
$.statement
)
)
),
// Title statement: matches the entire "TITLE = <text>" line as a single token
title_statement: $ => token(seq(
/[Tt][Ii][Tt][Ll][Ee]/,
/\s*=\s*/,
/[^\n\r]+/
)),
// Author/Family value: unquoted text with hyphens, underscores, slashes, etc.
// E.g. L-TECH, Siemens_Digital_Industries, Manz/Siemens (without quotes)
author_value: $ => token(repeat1(/[a-zA-Z0-9_\-\/]/)),
// --- Block definitions ---
type_definition: $ => seq(
alias(ci('TYPE'), $.keyword),
field('name', choice($.string, $.identifier)),
optional($.title_statement),
optional($.attribute_list),
optional(seq(alias(ci('NAME'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('AUTHOR'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('FAMILY'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('VERSION'), $.keyword), ':', $.number)),
alias(ci('STRUCT'), $.keyword),
repeat(choice($.var_item, $.block_comment)),
alias(ci('END_STRUCT'), $.keyword),
optional(';'),
alias(ci('END_TYPE'), $.keyword)
),
data_block: $ => seq(
alias(ci('DATA_BLOCK'), $.keyword),
field('name', choice($.string, $.identifier)),
optional($.title_statement),
optional($.attribute_list),
optional(seq(alias(ci('NAME'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('AUTHOR'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('FAMILY'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('VERSION'), $.keyword), ':', $.number)),
optional(choice(
alias(ci('NON_RETAIN'), $.keyword),
alias(ci('RETAIN'), $.keyword)
)),
// Associated FB/UDT name (for typed DBs: appears on its own line)
optional(field('associated', $.string)),
optional(choice(
$.var_declaration,
$.var_db_specific_declaration
)),
alias(ci('BEGIN'), $.keyword),
repeat($.statement),
alias(ci('END_DATA_BLOCK'), $.keyword)
),
organization_block: $ => seq(
alias(ci('ORGANIZATION_BLOCK'), $.keyword),
field('name', $.string),
optional($.title_statement),
optional($.attribute_list),
optional(seq(alias(ci('NAME'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('AUTHOR'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('FAMILY'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('VERSION'), $.keyword), ':', $.number)),
repeat($.var_declaration),
alias(ci('BEGIN'), $.keyword),
repeat($.statement),
alias(ci('END_ORGANIZATION_BLOCK'), $.keyword)
),
function_block: $ => seq(
alias(ci('FUNCTION_BLOCK'), $.keyword),
field('name', $.string),
optional($.title_statement),
optional($.attribute_list),
optional(seq(alias(ci('NAME'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('AUTHOR'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('FAMILY'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('VERSION'), $.keyword), ':', $.number)),
repeat(choice(
$.var_declaration,
$.var_temp_declaration,
$.var_constant_declaration,
$.var_retain_declaration,
$.var_non_retain_declaration,
$.var_db_specific_declaration,
$.block_comment
)),
alias(ci('BEGIN'), $.keyword),
repeat($.statement),
alias(ci('END_FUNCTION_BLOCK'), $.keyword)
),
function: $ => seq(
alias(ci('FUNCTION'), $.keyword),
field('name', $.string),
optional(seq(':', $.type)),
optional($.title_statement),
optional($.attribute_list),
optional(seq(alias(ci('NAME'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('AUTHOR'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('FAMILY'), $.keyword), ':', choice($.string, $.author_value))),
optional(seq(alias(ci('VERSION'), $.keyword), ':', $.number)),
repeat(choice(
$.var_declaration,
$.var_temp_declaration,
$.var_constant_declaration,
$.var_retain_declaration,
$.var_non_retain_declaration,
$.block_comment
)),
alias(ci('BEGIN'), $.keyword),
repeat($.statement),
alias(ci('END_FUNCTION'), $.keyword)
),
// --- Attributes ---
attribute_list: $ => seq(
'{',
$.attribute,
repeat(seq(choice(',', ';'), $.attribute)),
optional(choice(',', ';')),
'}'
),
attribute: $ => seq(
$.identifier,
':=',
$.attribute_value
),
attribute_value: $ => choice(
$.string,
alias(ci('TRUE'), $.constant),
alias(ci('FALSE'), $.constant),
$.identifier
),
// --- VAR section declarations ---
var_declaration: $ => seq(
choice(
alias(ci('VAR_INPUT'), $.keyword),
alias(ci('VAR_OUTPUT'), $.keyword),
alias(ci('VAR_IN_OUT'), $.keyword),
alias(ci('VAR'), $.keyword),
alias(ci('VAR_TEMP'), $.keyword),
seq(alias(ci('VAR'), $.keyword), alias(ci('CONSTANT'), $.keyword)),
seq(alias(ci('VAR'), $.keyword), alias(ci('RETAIN'), $.keyword)),
seq(alias(ci('VAR'), $.keyword), alias(ci('NON_RETAIN'), $.keyword)),
alias(ci('VAR_CONSTANT'), $.keyword),
alias(ci('VAR_RETAIN'), $.keyword),
alias(ci('VAR_NON_RETAIN'), $.keyword)
),
repeat(choice($.var_item, $.block_comment)),
alias(ci('END_VAR'), $.keyword)
),
var_temp_declaration: $ => seq(
alias(ci('VAR_TEMP'), $.keyword),
repeat(choice($.var_item, $.block_comment)),
alias(ci('END_VAR'), $.keyword)
),
var_constant_declaration: $ => seq(
choice(
seq(alias(ci('VAR'), $.keyword), alias(ci('CONSTANT'), $.keyword)),
alias(ci('VAR_CONSTANT'), $.keyword)
),
repeat(choice($.var_item, $.block_comment)),
alias(ci('END_VAR'), $.keyword)
),
var_retain_declaration: $ => seq(
choice(
seq(alias(ci('VAR'), $.keyword), alias(ci('RETAIN'), $.keyword)),
alias(ci('VAR_RETAIN'), $.keyword)
),
repeat(choice($.var_item, $.block_comment)),
alias(ci('END_VAR'), $.keyword)
),
var_non_retain_declaration: $ => seq(
choice(
seq(alias(ci('VAR'), $.keyword), alias(ci('NON_RETAIN'), $.keyword)),
alias(ci('VAR_NON_RETAIN'), $.keyword)
),
repeat(choice($.var_item, $.block_comment)),
alias(ci('END_VAR'), $.keyword)
),
var_db_specific_declaration: $ => seq(
seq(alias(ci('VAR'), $.keyword), alias(ci('DB_SPECIFIC'), $.keyword)),
repeat(choice($.var_item, $.block_comment)),
alias(ci('END_VAR'), $.keyword)
),
// --- Variable items ---
var_item: $ => seq(
field('name', $.identifier),
optional($.attribute_list),
':',
field('type', $.type),
optional(seq(':=', choice($.expression, $.struct_initializer))),
';'
),
// --- Types ---
type: $ => choice(
$.type_builtin,
$.array_type,
$.ref_type,
$.struct_type,
$.string_type,
$.identifier,
$.string
),
type_builtin: $ => choice(
alias(ci('BOOL'), $.type_builtin),
alias(ci('BYTE'), $.type_builtin),
alias(ci('WORD'), $.type_builtin),
alias(ci('DWORD'), $.type_builtin),
alias(ci('LWORD'), $.type_builtin),
alias(ci('SINT'), $.type_builtin),
alias(ci('USINT'), $.type_builtin),
alias(ci('INT'), $.type_builtin),
alias(ci('UINT'), $.type_builtin),
alias(ci('DINT'), $.type_builtin),
alias(ci('UDINT'), $.type_builtin),
alias(ci('LINT'), $.type_builtin),
alias(ci('ULINT'), $.type_builtin),
alias(ci('REAL'), $.type_builtin),
alias(ci('LREAL'), $.type_builtin),
alias(ci('CHAR'), $.type_builtin),
alias(ci('WCHAR'), $.type_builtin),
alias(ci('TIME'), $.type_builtin),
alias(ci('LTIME'), $.type_builtin),
alias(ci('S5TIME'), $.type_builtin),
alias(ci('DATE'), $.type_builtin),
alias(ci('TIME_OF_DAY'), $.type_builtin),
alias(ci('TOD'), $.type_builtin),
alias(ci('LTOD'), $.type_builtin),
alias(ci('LTIME_OF_DAY'), $.type_builtin),
alias(ci('DATE_AND_TIME'), $.type_builtin),
alias(ci('DT'), $.type_builtin),
alias(ci('LDT'), $.type_builtin),
alias(ci('DATE_AND_LTIME'), $.type_builtin),
alias(ci('DTL'), $.type_builtin),
alias(ci('VARIANT'), $.type_builtin)
),
string_type: $ => seq(
choice(
alias(ci('STRING'), $.type_builtin),
alias(ci('WSTRING'), $.type_builtin)
),
optional(seq('[', $.number, ']'))
),
array_type: $ => seq(
alias(ci('ARRAY'), $.keyword),
'[',
repeat1(seq(
choice(
seq(choice($.number, $.string, $.identifier), '..', choice($.number, $.string, $.identifier)),
'*'
),
optional(',')
)),
']',
alias(ci('OF'), $.keyword),
$.type
),
ref_type: $ => seq(
alias(ci('REF_TO'), $.keyword),
$.type
),
struct_type: $ => seq(
alias(ci('STRUCT'), $.keyword),
repeat(choice($.var_item, $.block_comment)),
alias(ci('END_STRUCT'), $.keyword)
),
// --- Statements ---
statement: $ => choice(
$.assignment,
$.fb_call,
$.if_statement,
$.case_statement,
$.for_statement,
$.while_statement,
$.repeat_statement,
$.region_statement,
$.function_call,
$.return_statement,
$.goto_statement,
$.exit_statement,
$.continue_statement,
$.empty_statement,
$.var_declaration,
$.var_temp_declaration,
$.var_constant_declaration,
$.var_retain_declaration,
$.var_non_retain_declaration,
$.var_db_specific_declaration
),
empty_statement: $ => ';',
// --- FB calls ---
fb_call: $ => seq(
field('instance', choice($.prefixed_identifier, $.identifier)),
'(',
optional($.fb_parameter_list),
')'
),
fb_parameter_list: $ => seq(
$.fb_parameter,
repeat(seq(',', $.fb_parameter))
),
fb_parameter: $ => seq(
field('name', $.identifier),
choice(':=', '=>'),
$.expression
),
// --- Assignments (including compound and chained) ---
assignment: $ => seq(
field('target', $.lvalue),
choice(':=', '+=', '-=', '*=', '/=', '&=', '|=', '^='),
field('value', choice($.expression, $.chained_assignment)),
';'
),
chained_assignment: $ => prec.right(seq(
$.lvalue,
':=',
choice($.expression, $.chained_assignment)
)),
lvalue: $ => choice(
$.identifier,
$.prefixed_identifier,
$.field_access,
$.array_access,
$.function_name,
$.bit_access
),
function_name: $ => $.string,
prefixed_identifier: $ => seq(alias('#', $.prefix), $.identifier),
field_access: $ => seq($.lvalue, '.', choice($.identifier, $.prefixed_identifier)),
array_access: $ => seq($.lvalue, '[', $.expression, ']'),
// Bit access: lvalue.%X0, lvalue.%X15, etc.
bit_access: $ => seq($.lvalue, '.', '%', /[A-Za-z]+/, $.number),
// --- Control flow ---
if_statement: $ => seq(
alias(ci('IF'), $.keyword),
$.expression,
alias(ci('THEN'), $.keyword),
repeat($.statement),
repeat($.elsif_clause),
optional($.else_clause),
alias(ci('END_IF'), $.keyword)
),
elsif_clause: $ => seq(
alias(ci('ELSIF'), $.keyword),
$.expression,
alias(ci('THEN'), $.keyword),
repeat($.statement)
),
else_clause: $ => seq(alias(ci('ELSE'), $.keyword), repeat($.statement)),
case_statement: $ => seq(
alias(ci('CASE'), $.keyword),
$.expression,
alias(ci('OF'), $.keyword),
repeat($.case_item),
optional($.else_clause),
alias(ci('END_CASE'), $.keyword)
),
case_item: $ => seq(
repeat1(seq(
choice($.number, $.string, $.identifier, $.prefixed_identifier, $.range),
optional(',')
)),
':',
repeat($.statement)
),
range: $ => seq($.number, '..', $.number),
for_statement: $ => seq(
alias(ci('FOR'), $.keyword),
choice($.identifier, $.prefixed_identifier),
':=',
$.expression,
alias(ci('TO'), $.keyword),
$.expression,
optional(seq(alias(ci('BY'), $.keyword), $.expression)),
alias(ci('DO'), $.keyword),
repeat($.statement),
alias(ci('END_FOR'), $.keyword)
),
while_statement: $ => seq(
alias(ci('WHILE'), $.keyword),
$.expression,
alias(ci('DO'), $.keyword),
repeat($.statement),
alias(ci('END_WHILE'), $.keyword)
),
repeat_statement: $ => seq(
alias(ci('REPEAT'), $.keyword),
repeat($.statement),
alias(ci('UNTIL'), $.keyword),
$.expression,
alias(ci('END_REPEAT'), $.keyword)
),
region_statement: $ => seq(
alias(ci('REGION'), $.keyword),
$.region_name,
repeat($.statement),
alias(ci('END_REGION'), $.keyword),
optional($.region_name)
),
region_name: $ => token(repeat1(choice(/[a-zA-Z0-9_\-\/]/, /[ \t]+/))),
return_statement: $ => alias(ci('RETURN'), $.keyword),
goto_statement: $ => seq(alias(ci('GOTO'), $.keyword), $.identifier),
// --- Function calls (with interleaved input/output params) ---
function_call: $ => choice(
seq(choice($.identifier, $.string), '(', optional($.parameter_list), ')'),
seq($.method_callee, '(', optional($.parameter_list), ')')
),
method_callee: $ => seq($.lvalue, '.', $.identifier),
parameter_list: $ => seq(
$.parameter,
repeat(seq(',', $.parameter))
),
parameter: $ => choice(
prec(1, seq(field('name', $.identifier), ':=', $.expression)),
prec(1, seq(field('name', $.identifier), '=>', $.identifier))
),
// --- Expressions ---
expression: $ => choice(
$.number,
$.hex_number,
$.binary_number,
$.octal_number,
$.time_value,
$.date_literal,
$.bool_literal,
$.typed_int_literal,
$.wstring_literal,
$.char_literal,
$.identifier,
$.prefixed_identifier,
$.field_access,
$.array_access,
$.bit_access,
$.string,
$.function_call,
$.unary_expression,
$.binary_expression,
prec(1, seq('(', $.expression, ')'))
),
unary_expression: $ => prec.right(10, choice(
seq(alias(ci('NOT'), $.operator), $.expression),
seq('+', $.expression),
seq('-', $.expression)
)),
binary_expression: $ => choice(
prec.left(2, seq($.expression, alias(ci('OR'), $.operator), $.expression)),
prec.left(3, seq($.expression, alias(ci('XOR'), $.operator), $.expression)),
prec.left(4, seq($.expression, alias(ci('AND'), $.operator), $.expression)),
prec.left(5, seq($.expression, choice('=', '<>'), $.expression)),
prec.left(6, seq($.expression, choice('<', '>', '<=', '>='), $.expression)),
prec.left(7, seq($.expression, choice('+', '-'), $.expression)),
prec.left(8, seq($.expression, choice('*', '/', alias(ci('MOD'), $.operator)), $.expression))
),
// Struct/array initializer: (TRUE, FALSE, 1, 2, ...) or ((1, 2), (3, 4)) or () or (name := value, ...)
struct_initializer: $ => prec(1, seq(
'(',
optional(seq(
choice(
$.struct_initializer,
seq(field('name', $.identifier), ':=', choice($.expression, $.struct_initializer)),
$.expression
),
repeat(seq(',', choice(
$.struct_initializer,
seq(field('name', $.identifier), ':=', choice($.expression, $.struct_initializer)),
$.expression
))),
optional(',')
)),
')'
)),
// --- Literals ---
number: $ => token(/\d+(\.\d+)?/),
hex_number: $ => token(seq('16#', /[0-9A-Fa-f_]+/)),
binary_number: $ => token(seq('2#', /[01_]+/)),
octal_number: $ => token(seq('8#', /[0-7_]+/)),
time_value: $ => token(seq(
/[Tt]#/,
repeat1(seq(/\d+/, choice(/[Nn][Ss]/, /[Uu][Ss]/, /[Mm][Ss]/, /[Ss]/, /[Mm][Ii][Nn]/, /[Hh]/, /[Dd]/))),
optional(/_[\d_]+[A-Za-z]+/)
)),
date_literal: $ => token(seq(
choice(
/[Dd][Aa][Tt][Ee]#/,
/[Tt][Oo][Dd]#/,
/[Dd][Tt]#/,
/[Ll][Tt][Ii][Mm][Ee]#/,
/[Ss]5[Tt][Ii][Mm][Ee]#/,
/[Ll][Dd][Tt]#/,
/[Ll][Tt][Oo][Dd]#/,
/[Dd][Tt][Ll]#/
),
/[\d:_\-]+/
)),
bool_literal: $ => token(seq(
/[Bb][Oo][Oo][Ll]#/,
choice('TRUE', 'FALSE', 'true', 'false', 'True', 'False')
)),
typed_int_literal: $ => token(seq(
choice(
/[Uu]?[Ss][Ii][Nn][Tt]#/,
/[Uu]?[Ii][Nn][Tt]#/,
/[Uu]?[Dd][Ii][Nn][Tt]#/,
/[Uu]?[Ll][Ii][Nn][Tt]#/,
/L#/,
/C#/,
/LW#/
),
/[\dA-Fa-f_#]+/
)),
string: $ => choice(
seq("'", repeat(choice(/[^'\n]/, "''")), "'"),
seq('"', repeat(choice(/[^"\n]/, '""')), '"')
),
wstring_literal: $ => token(seq(
choice(
/[Ww][Ss][Tt][Rr][Ii][Nn][Gg]#/,
/[Ss][Tt][Rr][Ii][Nn][Gg]#/
),
choice(
seq("'", repeat(choice(/[^'\n]/, "''")), "'"),
seq('"', repeat(choice(/[^"\n]/, '""')), '"')
)
)),
char_literal: $ => choice(
seq("'", /[^']/, "'"),
seq(/[Ww][Cc][Hh][Aa][Rr]#/, "'", /[^']/, "'"),
seq('C#', "'", /[^']/, "'")
),
identifier: $ => /[a-zA-Z_][a-zA-Z0-9_]*/,
line_comment: $ => token(seq('//', /.*/)),
block_comment: $ => token(seq(
'(*',
repeat(choice(/[^*]/, seq('*', /[^)]/))),
'*)'
)),
c_style_comment: $ => token(seq(
'(/*',
repeat(choice(/[^*]/, seq('*', /[^)]/))),
'*/)'
)),
exit_statement: $ => alias(ci('EXIT'), $.keyword),
continue_statement: $ => alias(ci('CONTINUE'), $.keyword),
// UTF-8 BOM (Byte Order Mark) - consumed at start of file
bom: $ => token('')
}
});