// 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 = " 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('') } });