diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0187b3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +*~ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..496d719 --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +GRAMMAR_NAME := st +GRAMMAR_SRC := grammar/$(GRAMMAR_NAME).bnf +BUILD_DIR := build/tree-sitter-$(GRAMMAR_NAME) +TESTS_DIR := tests + +TS_BNF_TOOL := ts-bnf-tool +TREE_SITTER := tree-sitter + +.DEFAULT_GOAL := help + +.PHONY: help check grammar test test-update clean + +help: ## Show this help message + @echo "StructuredCheck" + @echo + @echo "Usage: make " + @echo + @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_-]+:.*##/ {printf " %-12s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +check: ## Run ts-bnf-tool static checks on the BNF grammar + $(TS_BNF_TOOL) check $(GRAMMAR_SRC) + +grammar: $(BUILD_DIR)/src/parser.c ## Generate the tree-sitter parser from the BNF grammar + +$(BUILD_DIR)/src/parser.c: $(GRAMMAR_SRC) + rm -rf $(BUILD_DIR) + $(TS_BNF_TOOL) convert --generate --name $(GRAMMAR_NAME) --output-dir $(BUILD_DIR) $(GRAMMAR_SRC) + +test: grammar ## Run the corpus tests in tests/ against the generated parser + mkdir -p $(BUILD_DIR)/test/corpus + cp $(TESTS_DIR)/*.txt $(BUILD_DIR)/test/corpus/ + cd $(BUILD_DIR) && $(TREE_SITTER) test + +test-update: grammar ## Run the tests, update their expected trees, and copy them back to tests/ + mkdir -p $(BUILD_DIR)/test/corpus + cp $(TESTS_DIR)/*.txt $(BUILD_DIR)/test/corpus/ + cd $(BUILD_DIR) && $(TREE_SITTER) test --update + cp $(BUILD_DIR)/test/corpus/*.txt $(TESTS_DIR)/ + +clean: ## Remove generated build artifacts + rm -rf build diff --git a/grammar/configuration.bnf b/grammar/configuration.bnf new file mode 100644 index 0000000..6b44aef --- /dev/null +++ b/grammar/configuration.bnf @@ -0,0 +1,141 @@ + +# B.1.7 Configuration elements + +configuration_name -> identifier + ; + +resource_type_name -> identifier + ; + +configuration_declaration -> 'CONFIGURATION' configuration_name global_var_declarations? + _resources access_declarations? instance_specific_initializations? 'END_CONFIGURATION' + ; + +_resources -> single_resource_declaration + | resource_declaration+ + ; + +resource_declaration -> 'RESOURCE' resource_name 'ON' resource_type_name global_var_declarations? single_resource_declaration 'END_RESOURCE' + ; + +single_resource_declaration -> (task_configuration ';')* (program_configuration ';')+ + ; + +resource_name -> identifier + ; + +access_declarations -> 'VAR_ACCESS' (access_declaration ';')+ 'ENV_VAR' + ; + + +access_declaration -> access_name ':' access_path ':' _type_name_or_elementary direction? + ; + +# NOTE: deviates from the IEC grammar, which also builds an explicit +# `(resource_name '.')? (program_name '.')? ((identifier => fb_name) '.')*` +# prefix chain here before handing off to `symbolic_variable`. But +# `symbolic_variable` is already recursive through `structured_variable` / +# `record_variable` (variables.bnf), so `res.prog.fb1.var` already parses on +# its own as nested struct-field access - the explicit prefix chain was a +# second, redundant way to build the exact same dotted identifier sequence. +# Which segment is a resource, a program, an FB instance, or a struct field +# is a symbol-table question in any case, not something this CFG can answer, +# so we drop the redundant chain and rely on `symbolic_variable` alone. +access_path -> direct_variable + | symbolic_variable + ; + +# NOTE: deviates from the IEC grammar, which spells this out explicitly as +# `(resource_name '.')? global_var_name ('.' structure_element_name)?`. That +# shape is internally ambiguous on its own: given `a.b`, nothing tells the +# parser whether `a` is a `resource_name` (prefix present, no suffix) or `a` +# is the `global_var_name` itself (no prefix, `b` is the suffix +# `structure_element_name`) - both derive the same two-identifier chain. Same +# root cause as the `access_path` NOTE above (which segment is a resource, +# program, FB instance, or struct field is a symbol-table question, not a +# grammar one), so the fix is the same: drop the explicit prefix/suffix chain +# and rely on `symbolic_variable`'s own recursion through `structured_variable` +# to parse `a.b` unambiguously as nested struct-field access. +global_var_reference -> symbolic_variable + ; + + +access_name -> identifier + ; + +program_name -> identifier + ; + +direction -> 'READ_WRITE' | 'READ_ONLY' + ; + +task_configuration -> 'TASK' task_name task_initialization + ; + +task_name -> identifier + ; + +task_initialization -> '(' ('SINGLE' ':=' data_source ',')? ('INTERVAL' ':=' data_source ',')? 'PRIORITY' ':=' integer ')' + ; + +# NOTE: deviates from the IEC grammar, which offers `global_var_reference` +# and `program_output_reference` here instead of `access_path`. Both build a +# `(resource_name|program_name) '.' ...` prefix in front of what is, once +# you look past the labels, the same dotted-identifier chain `symbolic_variable` +# already parses via `structured_variable`'s recursion - see the comment on +# `access_path` above. Kept apart, they're ambiguous: `a.b.c` matches both +# `resource_name '.' global_var_name '.' structure_element_name` and +# `program_name '.' symbolic_variable` with no way to tell which without a +# symbol table. `access_path` already covers direct and symbolic variables. +data_source -> constant + | access_path + ; + +program_configuration -> 'PROGRAM' /(NON_)?RETAIN/? program_name ('WITH' task_name)? ':' program_type_name _prog_conf_elements? + ; + + +_prog_conf_elements -> '(' prog_conf_elements ')' + ; + +prog_conf_elements -> prog_conf_element (',' prog_conf_element)* + ; + +prog_conf_element -> fb_task | prog_cnxn + ; + +fb_task -> (identifier => fb_name) 'WITH' task_name + ; + +prog_cnxn -> symbolic_variable ':=' prog_data_source + | symbolic_variable '=>' data_sink + ; + +prog_data_source -> constant + | enumerated_value + | global_var_reference + | direct_variable + ; + +data_sink -> global_var_reference + | direct_variable + ; + +instance_specific_initializations -> 'VAR_CONFIG' (instance_specific_init ';')+ 'ENV_VAR' + ; + +instance_specific_init -> resource_name '.' program_name '.' ((identifier => fb_name) '.')* _instance_specific_init; + +# NOTE: deviates from the IEC grammar, which also offers +# `(identifier => fb_name) ':' function_block_type_name ':=' structure_initialization` +# here alongside `variable_name location? ':' located_var_spec_init` - same +# ambiguity as `external_declaration`/`global_var_decl` (variables.bnf): +# `located_var_spec_init` already reaches a bare type name via +# `_structure_element_declaration -> type_name_reference -> type_name`, and +# `type_name_reference`'s optional initializer already covers +# `structure_initialization`, so `fb_name : function_block_type_name := +# structure_initialization` is fully subsumed. Dropped. +_instance_specific_init -> variable_name location? ':' located_var_spec_init + ; + +# This syntax does not reflect the fact that location assignments are only allowed for references to variables which are marked by the asterisk notation at type declaration level. diff --git a/grammar/data_types.bnf b/grammar/data_types.bnf new file mode 100644 index 0000000..f359bdf --- /dev/null +++ b/grammar/data_types.bnf @@ -0,0 +1,244 @@ + +# B.1.3 + +# FIXME: Not used, only in library_element_name (not sure what that is) +# data_type_name -> non_generic_type_name +# | generic_type_name + #; + +# NOTE: the IEC grammar's `non_generic_type_name -> elementary_type_name | +# derived_type_name` used to live here. `derived_type_name` aliased a bare +# identifier six different ways (simple/subrange/enumerated/array/structure/ +# string type name - see the old `single_element_type_name` further down) +# with nothing syntactically to tell them apart. Every call site +# (`program_access_decl`, `access_declaration`, `array_specification`'s +# element type) only has `:=`/`;`/`direction` following it - never enough +# context to resolve which alias applies, which tree-sitter reports as an +# unresolvable conflict. Each call site now uses `_type_name_or_elementary` +# (further down) instead, which relies on the plain `type_name` for the +# derived case - same fix already applied to `_function_type_name` (pou.bnf). + +# B.1.3.1 Elementary data types + +elementary_type_name -> numeric_type_name + | date_type_name + | bit_string_type_name + | /W?STRING/ + | 'TIME' + ; + +numeric_type_name -> integer_type_name + | real_type_name + ; + +integer_type_name -> signed_integer_type_name + | unsigned_integer_type_name + ; + +signed_integer_type_name -> /[SDL]?INT/ + ; + + +unsigned_integer_type_name -> /U[SDL]?INT/ + ; + +real_type_name -> /L?REAL/ + ; + +date_type_name -> 'DATE' + | 'TIME_OF_DAY' + | 'TOD' + | 'DATE_AND_TIME' + | 'DT' + ; + +bit_string_type_name -> 'BOOL' + | 'BYTE' + | /[LD]?WORD/ + ; + + +# B.1.3.2 Generic data types + +# FIXME: used by data_type_name that is used by library_element_name, that I am not sure what it is about +# generic_type_name -> 'ANY' + #| /ANY_(DERIVED|ELEMENTARY|MAGNITUDE|NUM|REAL|INT|BIT|STRING|DATE)/ + #; + + +# B.1.3.3 Derived data types + +# NOTE: `derived_type_name` and `single_element_type_name` used to be defined +# here as: +# +# derived_type_name -> single_element_type_name +# | (identifier => array_type_name) +# | (identifier => structure_type_name) +# | (identifier => string_type_name) +# ; +# +# single_element_type_name -> (identifier => simple_type_name) +# | (identifier => subrange_type_name) +# | (identifier => enumerated_type_name) +# ; +# +# i.e. both were pure aliasing rules bundling the six bare-identifier type +# name aliases together with no other content. Once their only remaining +# caller, `non_generic_type_name`, was removed (see the NOTE above), they had +# no call sites left and were dropped rather than kept as unused grammar. The +# six aliases themselves (`simple_type_name` etc.) are still very much in +# use - directly, at each `type_declaration` alternative below - so only the +# two bundling rules went away, not the aliases. + +# NOTE: simple_type_name, subrange_type_name, enumerated_type_name, array_type_name, +# structure_type_name and string_type_name are all syntactically bare identifiers. +# They are kept as distinct node labels via aliasing (rather than as separate rules) +# so the parser only ever has one production to reduce an identifier through here; +# which label actually applies is only resolved once enough of the surrounding +# declaration has been parsed to tell the kinds apart. + +# NOTE: a declaration that is just `identifier` on the right of ':' is an alias to +# some other, already-declared type. Its actual kind (simple/subrange/enumerated/ +# array/structure) is not decidable from syntax alone - that requires a symbol +# table. So this is kept as ONE shared rule and referenced wherever a bare alias +# would otherwise be reachable through more than one of simple_specification, +# subrange_specification, enumerated_specification, array_specification or a +# structure alias - keeping it in more than one of those would make the parser +# choose between indistinguishable alternatives. +type_name -> identifier + ; + +# Same rationale as `_function_type_name` (pou.bnf): the derived case here +# would normally be `derived_type_name`, but that rule aliases a bare +# identifier six different ways with nothing to tell them apart +# syntactically, so a type name reachable through it with no following +# context to disambiguate is an unresolvable conflict. Used wherever a type +# name is followed by nothing more specific than `:=`/`;`/a `direction` - +# never enough to tell which alias applies: `program_access_decl`, +# `access_declaration`, and `array_specification`'s element type. +_type_name_or_elementary -> elementary_type_name + | type_name + ; + +_type_name_reference_init -> constant + | enumerated_value + | array_initialization + | structure_initialization + ; + +type_name_reference -> type_name (':=' _type_name_reference_init)? + ; + +data_type_declaration -> 'TYPE' (type_declaration ';')+ 'END_TYPE' + ; + +type_declaration -> single_element_type_declaration + | array_type_declaration + | structure_type_declaration + | string_type_declaration + | type_name_reference + ; + +single_element_type_declaration -> simple_type_declaration + | subrange_type_declaration + | enumerated_type_declaration + ; + +simple_type_declaration -> (identifier => simple_type_name) ':' simple_spec_init + ; + +simple_spec_init -> simple_specification (':=' constant)? + ; + +simple_specification -> elementary_type_name + ; + +subrange_type_declaration -> (identifier => subrange_type_name) ':' subrange_spec_init + ; + + +subrange_spec_init -> subrange_specification (':=' signed_integer)? + ; + +subrange_specification -> integer_type_name '(' subrange ')' + ; + +subrange -> signed_integer '..' signed_integer + ; + +enumerated_type_declaration -> (identifier => enumerated_type_name) ':' enumerated_spec_init + ; + +enumerated_spec_init -> enumerated_specification (':=' enumerated_value)? + ; + +enumerated_specification -> '(' enumerated_value (',' enumerated_value)* ')' + ; + +enumerated_value -> ((identifier => enumerated_type_name) '#') identifier + ; + +array_type_declaration -> (identifier => array_type_name) ':' array_spec_init + ; + +array_spec_init -> array_specification (':=' array_initialization)? + ; + +array_specification -> 'ARRAY' '[' subrange (',' subrange)* ']' 'OF' _type_name_or_elementary + ; + +array_initialization -> '[' array_initial_elements (',' array_initial_elements)* ']' + ; + +array_initial_elements -> array_initial_element + | integer '(' array_initial_element? ')' + ; + +array_initial_element -> constant + | enumerated_value + | structure_initialization + | array_initialization + ; + +structure_type_declaration -> (identifier => structure_type_name) ':' structure_specification + ; + +structure_specification -> structure_declaration + ; + +structure_declaration -> 'STRUCT' (structure_element_declaration ';')+ 'END_STRUCT' + ; + +structure_element_declaration -> structure_element_name ':' _structure_element_declaration + ; + +_structure_element_declaration -> simple_spec_init + | subrange_spec_init + | enumerated_spec_init + | array_spec_init + | type_name_reference + ; + +structure_element_name -> identifier + ; + +structure_initialization -> '(' structure_element_initialization (',' structure_element_initialization)* ')' + ; + +structure_element_initialization -> structure_element_name ':=' _structure_element_initialization + ; + +_structure_element_initialization -> constant + | enumerated_value + | array_initialization + | structure_initialization + ; + +# NOTE: the '[' length ']' is mandatory here. A bare STRING/WSTRING with no length +# and no initializer is already covered by simple_type_declaration, via +# elementary_type_name -> /W?STRING/ (see simple_specification). Making the +# length optional here as well would let both rules derive the same input +# (e.g. `Foo : STRING;`), which is an unresolvable grammar conflict. +string_type_declaration -> (identifier => string_type_name) ':' /W?STRING/ '[' integer ']' (':=' character_string)? + ; + diff --git a/grammar/graph.pdf b/grammar/graph.pdf new file mode 100644 index 0000000..36e4d66 Binary files /dev/null and b/grammar/graph.pdf differ diff --git a/grammar/literals.bnf b/grammar/literals.bnf new file mode 100644 index 0000000..187f202 --- /dev/null +++ b/grammar/literals.bnf @@ -0,0 +1,208 @@ + +%include 'data_types.bnf' + +# B.1.1 Identifiers + +identifier -> /([A-Za-z]|_[A-Za-z0-9])(_?[A-Za-z0-9])*/ + ; + +# B.1.2 Constants + +constant -> numeric_literal + | character_string + | time_literal + | bit_string_literal + | boolean_literal + ; + +# B.1.2.1 Numeric literals + + +numeric_literal -> integer_literal + | real_literal + ; + +integer_literal -> (integer_type_name '#')? _integer_literal + ; + +_integer_literal -> signed_integer + | binary_integer + | octal_integer + | hex_integer + ; + +# NOTE: the sign is folded into the regex (rather than `('+'|'-')? integer`) +# so this is a single token. As two grammar symbols, the leading '-' would be +# the same terminal `unary_operator` uses, and at a position like `IF -5` +# the parser can't tell which production is consuming it - a genuine +# parser-level ambiguity. As one token, tree-sitter's lexer resolves it by +# longest match: `-5` (2 chars) wins over the lone `-` (1 char) wherever both +# are valid, so `unary_operator` only ever sees a bare sign when no digits +# immediately follow it. +signed_integer -> /[+-]?[0-9](_?[0-9])*/ + ; + +integer -> /[0-9](_?[0-9])*/ + ; + +binary_integer -> '2#' /[01](_?[01])*/ + ; + +octal_integer -> '8#' /[0-7](_?[0-7])*/ + ; + +hex_integer -> '16#' /[0-9A-F](_?[0-9A-F])*/ + ; + +# FIXME: If spaces are possible around exponents, for instance, we need to check the regex +# NOTE: sign folded into the regex, same reasoning as `signed_integer` above. +real_literal -> (real_type_name '#')? /[+-]?[0-9](_?[0-9])*\.[0-9](_?[0-9])*([Ee][+-]?[0-9](_?[0-9])*)?/ + ; + +# NOTE: deviates from the IEC grammar, where the type prefix here is optional. +# With it optional, an unprefixed integer is ambiguous between this rule and +# signed_integer (both reduce a bare `integer` with nothing else), which the +# IEC text only disambiguates via the semantics/context of the assignment +# target - something this CFG can't express. We require the prefix so the +# grammar stays unambiguous; recovering untagged bit-string literals, if +# needed, has to happen in a later semantic/type-checking pass. +bit_string_literal -> ('BYTE' | /[DL]?WORD/) '#' _unsigned_integer_literal + ; + +_unsigned_integer_literal -> integer + | binary_integer + | octal_integer + | hex_integer + ; + +boolean_literal -> 'BOOL#'? /[10]/ | 'TRUE' | 'FALSE' + ; + + +# B.1.2.2 Character Strings + +# NOTE: deviates from the IEC grammar, which offers `character_string` here as +# a choice between single_byte_character_string and double_byte_character_string +# - both delimited the same way, with mostly-overlapping representation sets. +# Even the escapes are ambiguous on their own: `$AAAA` is either one 4-digit +# escape, or a 2-digit escape `$AA` followed by two literal characters 'A','A' +# - so no amount of scanning tells single-byte from double-byte here. Which +# one it is only becomes knowable once the target's declared type (STRING vs +# WSTRING) is known - semantics/context, not this CFG. `single_byte_character_string` +# and `double_byte_character_string` stay directly usable where a preceding +# 'STRING'/'WSTRING' keyword already disambiguates (see variables.bnf); here, +# with no such keyword around, we use one shared, unaliased rule and defer the +# single/double-byte distinction to a later semantic pass. +character_string -> "'" character_representation* "'" + ; + +character_representation -> common_character_representation + | "$'" + | '"' + | /\$[0-9A-F][0-9A-F]([0-9A-F][0-9A-F])?/ + ; + +single_byte_character_string -> "'" single_byte_character_representation* "'" + ; + +double_byte_character_string -> "'" double_byte_character_representation* "'" + ; + +single_byte_character_representation -> common_character_representation + | "$'" + | '"' + | /\$[0-9A-F][0-9A-F]/ + ; + +double_byte_character_representation -> common_character_representation + | "$'" + | '"' + | /\$[0-9A-F][0-9A-F][0-9A-F][0-9A-F]/ + ; + +common_character_representation -> _printable_character + | /\$[$LNPRTlnprt]/ + ; + +# any printable character except '$', '"' or "'" +_printable_character -> /[\x20-\x23\x25\x26\x28-\x7E\x80-\xFF]/ + ; + + +# B.1.2.3 Time Literals + +time_literal -> duration + | time_of_day + | date + | date_and_time + ; + +# B.1.2.3.1 Duration + +duration -> /T(IME)?/ '#' '-'? interval + ; + +interval -> days + | hours + | minutes + | seconds + | milliseconds + ; + +days -> fixed_point 'd' | integer 'd' '_'? hours + ; + +fixed_point -> /[0-9]+(\.[0-9]+)?/ + ; + +hours -> fixed_point 'h' | integer 'h' '_'? minutes + ; + +minutes -> fixed_point 'm' | integer 'm' '_'? seconds + ; + +seconds -> fixed_point 's' | integer 's' '_'? milliseconds + ; + +milliseconds -> fixed_point 'ms' + ; + + +# B.1.2.3.2 Time of day and date + +time_of_day -> ('TIME_OF_DAY' | 'TOD') '#' daytime + ; + +daytime -> day_hour ':' day_minute ':' day_second + ; + +# FIXME: should we make regex to validate ranges? +day_hour -> integer + ; + +day_minute -> integer + ; + +day_second -> fixed_point + ; + + +date -> /D(ATE)?/ '#' date_literal + ; + +date_literal -> year '-' month '-' day + ; + +year -> integer + ; + +month -> integer + ; + +day -> integer + ; + +date_and_time -> ('DATE_AND_TIME' | 'DT') '#' date_literal '-' daytime + ; + + diff --git a/grammar/pou.bnf b/grammar/pou.bnf new file mode 100644 index 0000000..1a84579 --- /dev/null +++ b/grammar/pou.bnf @@ -0,0 +1,137 @@ + +%include 'variables.bnf' + +# B.1.5 Program organization units + +# B.1.5.1 Functions + +function_name -> standard_function_name + | derived_function_name + ; + +# FIXME: Complete standard function names from 2.5.1.5 +standard_function_name -> 'FIXME' + ; + +derived_function_name -> identifier + ; + +function_declaration -> 'FUNCTION' derived_function_name ':' _function_type_name? _function_decls function_body 'END_FUNCTION' + ; + +# NOTE: deviates from the IEC grammar, where the derived case here is +# `derived_type_name`. That rule aliases a bare identifier six different ways +# (simple/subrange/enumerated/array/structure/string type name) with nothing +# to tell them apart syntactically - which kind it is can only be known once +# it's looked up in a symbol table, i.e. semantics/context, not this CFG. We +# use the plain, unaliased `type_name` instead, same as `type_name_reference` +# does for the analogous "identifier refers to some other declared type" case. +_function_type_name -> elementary_type_name + | type_name + ; + +_function_decls -> io_var_declarations + | function_var_decls + ; + +io_var_declarations -> input_declarations + | output_declarations + | input_output_declarations + ; + +function_var_decls -> 'VAR' 'CONSTANT'? (var2_init_decl ';')+ 'END_VAR' + ; + +# FIXME: Confirm we can simplify this way +function_body -> sequential_function_chart | statement_list + ; + + +var2_init_decl -> var1_init_decl + | array_var_init_decl + | structured_var_init_decl + | string_var_declaration + ; + +# FIXME: NOTE 1 This syntax does not reflect the fact that each function must have at least one input declaration. + +# FIXME: NOTE 2 This syntax does not reflect the fact that edge declarations, function block references and invocations are not allowed in function bodies + +# B.1.5.2 Function blocks + +# NOTE: unlike the IEC grammar, this does not also offer +# `derived_function_block_name` here. That case is a bare identifier, +# indistinguishable from `type_name` wherever both would be reachable at the +# same position (see the NOTEs around `var_init_decl` and +# `structured_var_declaration` in variables.bnf) - callers that need to +# accept a user-declared FB type already do so via `type_name`/ +# `type_name_reference`. This rule only needs to cover the standard names, +# which are fixed keyword literals and so don't collide with `type_name` the +# way a second bare-identifier rule would. +function_block_type_name -> standard_function_block_name + ; + +# IEC 61131-3:2003 ยง2.5.2.3 (tables 34-38): the fixed set of standard +# function blocks common to every conformant implementation. +# - Bistable (2.5.2.3.1): SR (set-dominant), RS (reset-dominant) +# - Edge detection (2.5.2.3.2): R_TRIG, F_TRIG +# - Counters (2.5.2.3.3): CTU/CTD/CTUD, each with typed variants suffixed +# by their PV/CV type. CTU and CTD each have all five (INT is the +# unsuffixed base name, plus _DINT/_LINT/_UDINT/_ULINT); CTUD only has +# four - the standard's own table 36 has no CTUD_UDINT. +# - Timers (2.5.2.3.4): TP (pulse), TON (on-delay), TOF (off-delay). Table +# 37 also lists graphical-only shorthand forms, but "in textual +# languages, features 2b and 3b shall not be used" - not relevant here. +# Communication function blocks (2.5.2.3.5) are deferred entirely to +# IEC 61131-5, with no concrete names given in this part. +standard_function_block_name -> 'SR' | 'RS' + | 'R_TRIG' | 'F_TRIG' + | 'CTU' | 'CTU_DINT' | 'CTU_LINT' | 'CTU_UDINT' | 'CTU_ULINT' + | 'CTD' | 'CTD_DINT' | 'CTD_LINT' | 'CTD_UDINT' | 'CTD_ULINT' + | 'CTUD' | 'CTUD_DINT' | 'CTUD_LINT' | 'CTUD_ULINT' + | 'TP' | 'TON' | 'TOF' + ; + +derived_function_block_name -> identifier + ; + +function_block_declaration -> 'FUNCTION_BLOCK' derived_function_block_name _function_decls? function_block_body 'END_FUNCTION_BLOCK' + ; + +other_var_declarations -> external_var_declarations + | var_declarations + | retentive_var_declarations + | non_retentive_var_declarations + | temp_var_decls + | incompl_located_var_declarations + ; + +temp_var_decls -> 'VAR_TEMP' (temp_var_decl ';')+ 'END_VAR' + ; + +non_retentive_var_declarations -> 'VAR' 'NON_RETAIN' (var_init_decl ';')+ 'END_VAR' + ; + +function_block_body -> sequential_function_chart | statement_list + ; + +# B.1.5.3 Programs + +program_type_name -> identifier + ; + +program_declaration -> 'PROGRAM' program_type_name _program_decls function_block_body 'END_PROGRAM' + ; + +_program_decls -> io_var_declarations + | other_var_declarations + | located_var_declarations + | program_access_decls + ; + +program_access_decls -> 'VAR_ACCESS' program_access_decl ';' (program_access_decl ';')* 'END_VAR' + ; + +program_access_decl -> access_name ':' symbolic_variable ':' _type_name_or_elementary direction? + ; + diff --git a/grammar/railroad.svg b/grammar/railroad.svg new file mode 100644 index 0000000..21e6c76 --- /dev/null +++ b/grammar/railroad.svg @@ -0,0 +1,1238 @@ + + + + +bit_datatype + + + + + + + + +'BOOL' + + + + + + + + +'BYTE' + + + + + +'WORD' + + + + + +'DWORD' + + + + + + + + + +integer_datatype + + + + + + +/U?[DS]?INT/ + + + + + + + + +float_datatype + + + + + + +/L?REAL/ + + + + + + + + +literal + + + + + + + + + +integer + + + + + + + + + + +floating_pointer_number + + + + + + + +time_literal + + + + + + + +character_string + + + + + + + + + + +integer + + + + + + + + + + +_datatype + + + + + + + + + + + + + +'+' + + + + + + +'-' + + + + + + + +decimal_digit_string + + + + + + + + + + + + +binary_digit_string + + + + + + + +octal_digit_string + + + + + + + +hex_digit_string + + + + + + + + + + + + +decimal_digit_string + + + + + + +/[0-9]+(_[0-9]+)*/ + + + + + + + + +binary_digit_string + + + + + + + +'2#' + + + + +/[01]+(_[01]+)*/ + + + + + + + + + + +octal_digit_string + + + + + + + +'8#' + + + + +/[0-7]+(_[0-7]+)*/ + + + + + + + + + + +hex_digit_string + + + + + + + +'16#' + + + + +/[0-9A-F]+(_[0-9A-F]+)*/ + + + + + + + + + + +floating_pointer_number + + + + + + + + + + + +float_datatype + + + + + +'#' + + + + + + + + + + + +'+' + + + + + + +'-' + + + + + + + +decimal_digit_string + + + + + + +'.' + + + + + +decimal_digit_string + + + + + + + + +_exponent + + + + + + + + + + + + + + +time_literal + + + + + + + + + +date + + + + + + + + + + +time_of_day + + + + + + + +date_and_time + + + + + + + +duration + + + + + + + + + + +date + + + + + + + +/D(ATE)?#/ + + + + + +date_information + + + + + + + + + + + +duration + + + + + + + +/T(IME)?#/ + + + + + + + +decimal_representation + + + + + + + + + +sequence_representation + + + + + + + +'_' + + + + + + + + +decimal_representation + + + + + + + + + + + + + + + + +time_of_day + + + + + + + +/(TIME_OF_DAY|TOD)#/ + + + + + +time_of_day_information + + + + + + + + + + + +date_and_time + + + + + + + +/(DATE_AND_TIME|DT)#/ + + + + + +date_information + + + + + +'-' + + + + + +time_of_day_information + + + + + + + + + + + + + +date_information + + + + + + +/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/ + + + + + + + + +time_of_day_information + + + + + + +/([0-9]|1[0-9]|2[0-3]):[0-5]?[0-9]:[0-5]?[0-9](\.[0-9][0-9]?[0-9]?)?/ + + + + + + + + +sequence_representation + + + + + + + + + + +_days + + + + + + + + +_hours + + + + + + + + + +_minutes + + + + + + + + + +_seconds + + + + + + + + + +_milliseconds + + + + + + + + + + + + + + + + + + +_hours + + + + + + + + +_minutes + + + + + + + + + +_seconds + + + + + + + + + +_milliseconds + + + + + + + + + + + + + +_minutes + + + + + + + + +_seconds + + + + + + + + + +_milliseconds + + + + + + + + + + + + +_seconds + + + + + + + + +_milliseconds + + + + + + + + + + +_milliseconds + + + + + + + + + + +_days + + + + + + + + +decimal_digit_string + + + + + +'d' + + + + + + +'_' + + + + + + + + + + + + +_hours + + + + + + + +/[01]?[0-9]|2[0-3]/ + + + + +'h' + + + + + + +'_' + + + + + + + + + + + + +_minutes + + + + + + + +/[0-5]?[0-9]/ + + + + +'m' + + + + + + +'_' + + + + + + + + + + + + +_seconds + + + + + + + +/[0-5]?[0-9]/ + + + + +'s' + + + + + + +'_' + + + + + + + + + + + + +_milliseconds + + + + + + + +/[0-9][0-9]?[0-9]?/ + + + + +'ms' + + + + + + + + + + +decimal_representation + + + + + + + + +decimal_digit_string + + + + + +'.' + + + + + + + +decimal_representation + + + + + + + + +'d' + + + + + + + + + +'h' + + + + + +'m' + + + + + +'s' + + + + + +'ms' + + + + + + + + + + + + + +character_string + + + + + + + + + +'STRING#' + + + + + +'\'' + + + + + + + + + + + +characters + + + + + + + +'\'' + + + + + + + + + + + + +characters + + + + + + + + +/$[0-9A-F][0-9A-F]/ + + + + + + + +/[\x20-\x23\x25\x26\x28-\x7E\x80-\xFF]/ + + + + + +/$['$LINnPpRrTt]/ + + + + + + + + + +_exponent + + + + + + + + + +'E' + + + + + + +'e' + + + + + + + + + +'+' + + + + + + +'-' + + + + + + + +decimal_digit_string + + + + + + + + + + + + +_datatype + + + + + + + + + + +bit_datatype + + + + + + + + +integer_datatype + + + + + + +'#' + + + + + + + + + + \ No newline at end of file diff --git a/grammar/sfc.bnf b/grammar/sfc.bnf new file mode 100644 index 0000000..e0d2ec5 --- /dev/null +++ b/grammar/sfc.bnf @@ -0,0 +1,66 @@ + +%include "pou.bnf" + +# B.1.6 Sequential function chart elements + +sequential_function_chart -> sfc_network+ + ; + +sfc_network -> initial_step (step | transition | action)* + ; + +initial_step -> 'INITIAL_STEP' step_name ':' (action_association ';')* 'END_STEP' + ; + +step -> 'STEP' step_name ':' (action_association ';')* 'END_STEP' + ; + +step_name -> identifier; + +# FIXME: this would allow a comma right after the parenthesis, what seems wrong - to confirm +action_association -> action_name '(' action_qualifier? (',' indicator_name)* ')' + ; + +action_name -> identifier + ; + +action_qualifier -> /[NRSP]/ + | timed_qualifier ',' action_time + ; + +timed_qualifier -> 'L' | 'D' | 'SD' | 'DS' | 'SL' + ; + +action_time -> duration + | variable_name + ; + +indicator_name -> variable_name + ; + +transition -> 'TRANSITION' transition_name? _priority? 'FROM' steps 'TO' steps transition_condition 'END_TRANSITION' + ; + + +_priority -> '(' 'PRIORITY' ':=' integer ')' + ; + +transition_name -> identifier + ; + +steps -> step_name + | '(' step_name (',' step_name)+ ')' + ; + +transition_condition -> +# FIXME: this one's for IL +# ':' simple_instruction_list + ':=' expression ';' +# FIXME: I suppose this is only for ladder diagrams and function block diagrams +# | ':' (fbd_network | rung) + ; + +action -> 'ACTION' action_name ':' function_block_body 'END_ACTION' + ; + +# NOTE 1 The non-terminals simple_instruction_list and expression are defined in B.2.1 and B.3.1, respectively. diff --git a/grammar/st.bnf b/grammar/st.bnf new file mode 100644 index 0000000..fbf2b41 --- /dev/null +++ b/grammar/st.bnf @@ -0,0 +1,185 @@ + +%include "literals.bnf" +%include "pou.bnf" +%include "sfc.bnf" +%include "configuration.bnf" + +# param_assignment's `'NOT'? variable_name '=>' variable` (negate an FB's +# output parameter on the way out) and unary_operator's `'NOT' primary_expression` +# (boolean negation in an expression) both start with 'NOT' identifier, and +# there's no precedence that makes one "win" over the other in general - +# whether a given `NOT x` belongs to the output form is only known once '=>' +# does or doesn't turn up right after. This is genuinely ambiguous locally, +# not something restructuring the grammar can remove, so we hand it to GLR. +# Tree-sitter's default conflict heuristic (prefer the longer/shift parse) +# gives the right answer: it tries the `'=>' variable` continuation first and +# falls back to a plain boolean-negation expression when '=>' isn't there. +# Verified against `NOT ENO => flag`, `x := NOT ENO`, and bare `NOT ENO`. +%conflicts [param_assignment, unary_operator] + +# statement_list vs case_element: identifier after a statement could start +# a new assignment_statement or the next case_element's enumerated_value. +# Only disambiguated by the token after (# vs :=/(), so let GLR fork here. +%conflicts [statement_list] + +# B.0 Programming Model + +library_element_declaration -> data_type_declaration + | function_declaration + | function_block_declaration + | program_declaration + | configuration_declaration + ; + +# IEC 61131-3 supports `(* ... *)` block comments (2003 edition, table B.1.7 +# lexical elements) and `//` line comments (a near-universal vendor extension, +# not in the 2003 text but present in virtually every real-world ST codebase - +# see tests/community_samples.txt, which uses both). Declared via `%extras` +# so a comment can appear anywhere whitespace can, without every rule needing +# to account for it explicitly. Kept below `library_element_declaration` +# deliberately: the generated parser's start rule defaults to the first rule +# written in this file, and `library_element_declaration` is the only rule +# that makes sense as "the thing this grammar parses" - putting `comment` +# ahead of it would silently hijack the start rule. +comment -> /\(\*[\s\S]*?\*\)/ + | /\/\/[^\n]*/ + ; + +%extras /\s/, comment + +# B.3.1 Expressions + +expression -> xor_expression ('OR' xor_expression)* + ; + +xor_expression -> and_expression ('XOR' and_expression)* + ; + +and_expression -> comparison (('&'|'AND') comparison)* + ; + +comparison -> equ_expression (('='|'<>') equ_expression)* + ; + + +equ_expression -> add_expression (comparison_operator add_expression)* + ; + +comparison_operator -> '<' | '>' | '<=' | '>=' + ; + +add_expression -> term (add_operator term)* + ; + +add_operator -> '+' | '-' + ; + +term -> power_expression (multiply_operator power_expression)* + ; + +multiply_operator -> '*' | '/' | 'MOD' + ; + +power_expression -> unary_expression ('**' unary_expression)* + ; + +unary_expression -> unary_operator? primary_expression + ; + +unary_operator -> '-' | 'NOT' + ; + +primary_expression -> constant + | enumerated_value + | variable + | '(' expression ')' + | function_name '(' param_assignment (',' param_assignment)* ')' + ; + + +# B3.2 Statements + +statement_list -> (statement? ';')+ + ; + +statement -> assignment_statement + | subprogram_control_statement + | selection_statement + | iteration_statement + ; + +# B.3.2.1 Assignment Statements + +assignment_statement -> variable ':=' expression + ; + + +# B.3.2.2 Subprogram control statements + +subprogram_control_statement -> fb_invocation + | 'RETURN' + ; + +fb_invocation -> (identifier => fb_name) '(' (param_assignment (',' param_assignment)*)? ')' + ; + +param_assignment -> (variable_name ':=')? expression + | 'NOT'? variable_name '=>' variable + ; + +# B.3.2.3 Selection Statements + +selection_statement -> if_statement + | case_statement + ; + +if_statement -> 'IF' expression 'THEN' statement_list _elsif* _else? 'END_IF' + ; + + +_elsif -> 'ELSIF' expression 'THEN' statement_list + ; + +_else -> 'ELSE' statement_list + ; + +case_statement -> 'CASE' expression 'OF' case_element+ _else? 'END_CASE' + ; + +case_element -> case_list ':' statement_list + ; + + +case_list -> case_list_element (',' case_list_element)* + ; + +case_list_element -> subrange + | signed_integer + | enumerated_value + ; + +# B.3.2.4 Iteration Statements + +iteration_statement -> for_statement + | while_statement + | repeat_statement + | exit_statement + ; + +for_statement -> 'FOR' control_variable ':=' for_list 'DO' statement_list 'END_FOR' + ; + +control_variable -> identifier + ; + +for_list -> expression 'TO' expression ('BY' expression)? + ; + +while_statement -> 'WHILE' expression 'DO' statement_list 'END_WHILE' + ; + +repeat_statement -> 'REPEAT' statement_list 'UNTIL' expression 'END_REPEAT' + ; + +exit_statement -> 'EXIT' + ; diff --git a/grammar/variables.bnf b/grammar/variables.bnf new file mode 100644 index 0000000..9565ba3 --- /dev/null +++ b/grammar/variables.bnf @@ -0,0 +1,245 @@ + +%include 'literals.bnf' + +# B.1.4 Variables + +variable -> direct_variable + | symbolic_variable + ; + +symbolic_variable -> variable_name + | multi_element_variable + ; + +variable_name -> identifier + ; + + +# B.1.4.1 Directly represented variables + +direct_variable -> '%' location_prefix size_prefix? integer ('.' integer)* + ; + +location_prefix -> /[IQM]/ + ; + +# FIXME: Grammar defines 'NIL', which I suppose is empty... +size_prefix -> /[XBWDL]/ + ; + +# B.1.4.2 Multi-element variables + +multi_element_variable -> array_variable + | structured_variable + ; + +array_variable -> subscripted_variable subscript_list + ; + +subscripted_variable -> symbolic_variable + ; + +subscript_list -> '[' subscript (',' subscript)* ']' + ; + +subscript -> expression + ; + +structured_variable -> record_variable '.' field_selector + ; + +record_variable -> symbolic_variable + ; + +field_selector -> identifier + ; + +# B.1.4.3 Declaration and initialization + +input_declarations -> 'VAR_INPUT' /(NON_)?RETAIN/? (input_declaration ';')+ 'END_VAR' + ; + +input_declaration -> var_init_decl + | edge_declaration + ; + +edge_declaration -> var1_list ':' 'BOOL' /[RF]_EDGE/ + ; + +# NOTE: deviates from the IEC grammar's `fb_name_decl` +# (`(var1_list => fb_name_list) ':' function_block_type_name (':=' structure_initialization)?`) +# in one respect: the derived-FB-type half of `function_block_type_name` is +# dropped, since `derived_function_block_name -> identifier` is a bare +# identifier with nothing to distinguish it from `type_name` in +# `structured_var_init_decl` below - "this names a FUNCTION_BLOCK" vs "this +# names a TYPE" is only knowable from a symbol table, i.e. semantics/context, +# not this CFG. A derived FB instance like `myTimer : MyCustomFb;` is still +# accepted, just folded into `structured_var_init_decl` like any other +# type reference. What's kept is the standard-FB-type half: since +# `function_block_type_name` is now restricted to `standard_function_block_name` +# (fixed keyword literals - see pou.bnf), it no longer collides with +# `type_name`, so `fb_instantiation` below reinstates it for names like +# `myTimer : TON := (PT := t#100ms);`. The `fb_name_list` alias from the IEC +# grammar isn't reinstated - `var1_list` alone is used, same as the other +# alternatives here. +var_init_decl -> var1_init_decl + | array_var_init_decl + | structured_var_init_decl + | string_var_declaration + | fb_instantiation + ; + +fb_instantiation -> var1_list ':' function_block_type_name (':=' structure_initialization)? + ; + +var1_init_decl -> var1_list ':' _var1_init_decl + ; + +_var1_init_decl -> simple_spec_init + | subrange_spec_init + | enumerated_spec_init + ; + +var1_list -> variable_name (',' variable_name)* + ; + +array_var_init_decl -> var1_list ':' array_spec_init + ; + +structured_var_init_decl -> var1_list ':' type_name_reference + ; + +output_declarations -> 'VAR_OUTPUT' /(NON_)?RETAIN/? (var_init_decl ';')+ 'END_VAR' + ; + +input_output_declarations -> 'VAR_IN_OUT' (var_declaration ';')+ 'END_VAR' + ; + +# NOTE: deviates from the IEC grammar, which also offers `fb_name_decl` as an +# alternative here, same reasoning as `var_init_decl` above: whether a bare +# `foo : Bar;` names a FUNCTION_BLOCK instance or a struct-typed variable +# isn't decidable without a symbol table, and `fb_name_decl`'s only other +# content, `(':=' structure_initialization)?`, is optional and VAR_IN_OUT +# doesn't allow initializers anyway - so it adds nothing `structured_var_declaration` +# doesn't already accept. Dropped here (see `var_init_decl` above for where +# `fb_name_decl` itself was removed). +var_declaration -> temp_var_decl + ; + +temp_var_decl -> var1_declaration + | array_var_declaration + | structured_var_declaration + | string_var_declaration + ; + +var1_declaration -> var1_list ':' _var1_declaration + ; + +_var1_declaration -> simple_specification + | subrange_specification + | enumerated_specification + ; + +array_var_declaration -> var1_list ':' array_specification + ; + +# NOTE: uses `type_name`, not `(identifier => structure_type_name)` as the +# IEC grammar does - since this now also covers the merged FUNCTION_BLOCK +# case (see `var_declaration` above), aliasing it specifically as +# "structure_type_name" would be misleading; which of the two it actually is +# isn't decidable here anyway. +structured_var_declaration -> var1_list ':' type_name + ; + +var_declarations -> 'VAR' 'CONSTANT'? (var_init_decl ';')+ 'END_VAR' + ; + +retentive_var_declarations -> 'VAR' 'RETAIN' (var_init_decl ';')+ 'END_VAR' + ; + +located_var_declarations -> 'VAR' ('CONSTANT' | /(NON_)?RETAIN/) ? (located_var_decl ';')+ 'END_VAR' + ; + +located_var_decl -> variable_name? location ':' located_var_spec_init + ; + +external_var_declarations -> 'VAR_EXTERNAL' 'CONSTANT'? (external_declaration ';')+ 'END_VAR' + ; + +external_declaration -> global_var_name ':' _external_declaration + ; + +# NOTE: deviates from the IEC grammar, which also offers `function_block_type_name` +# here alongside `type_name` - same ambiguity as above (both reduce to a bare +# identifier, and which kind it is isn't decidable without a symbol table). +# Dropped in favor of `type_name` alone. +_external_declaration -> simple_specification + | subrange_specification + | enumerated_specification + | structured_variable + | type_name + ; + +global_var_name -> identifier + ; + +global_var_declarations -> 'VAR_GLOBAL' ('CONSTANT'|'RETAIN')? (global_var_decl ';')+ 'END_VAR' + ; + +# NOTE: deviates from the IEC grammar, which also offers `function_block_type_name` +# here alongside `located_var_spec_init` - same ambiguity as `_external_declaration` +# above: `located_var_spec_init` already reaches `type_name` via +# `_structure_element_declaration -> ... | type_name_reference` +# (data_types.bnf), so `function_block_type_name` was a redundant, ambiguous +# alternative. Dropped. +global_var_decl -> global_var_spec ':' located_var_spec_init? + ; + +global_var_spec -> global_var_list + | global_var_name? location + ; + +located_var_spec_init -> _structure_element_declaration + | single_byte_string_spec + | double_byte_string_spec + ; + +location -> 'AT' direct_variable + ; + +global_var_list -> global_var_name (',' global_var_name)* + ; + +string_var_declaration -> single_byte_string_var_declaration + | double_byte_string_var_declaration + ; + +single_byte_string_var_declaration -> var1_list ':' single_byte_string_spec + ; + +single_byte_string_spec -> 'STRING' ('[' integer ']')? (':=' single_byte_character_string)? + ; + + +double_byte_string_var_declaration -> var1_list ':' double_byte_string_spec + ; + +double_byte_string_spec -> 'WSTRING' ('[' integer ']')? (':=' double_byte_character_string)? + ; + +incompl_located_var_declarations -> 'VAR' /(NON_)?RETAIN/? (incompl_located_var_decl ';')+ 'END_VAR' + ; + +incompl_located_var_decl -> variable_name incompl_location ':' var_spec + ; + +incompl_location -> 'AT' '%' /[IQM]/ '*' + ; + +var_spec -> simple_specification + | subrange_specification + | enumerated_specification + | array_spec_init + | type_name + | /W?STRING/ ('[' integer ']') + ; diff --git a/tests/FINDINGS.md b/tests/FINDINGS.md new file mode 100644 index 0000000..8f9fd9f --- /dev/null +++ b/tests/FINDINGS.md @@ -0,0 +1,219 @@ +# Findings from testing against real-world ST code + +`community_samples.txt` (see `SOURCES.md` for provenance) surfaced four +grammar gaps beyond the tree-sitter conflict fixes. This documents each one: +the failing example, the root cause, and a recommended fix. No grammar +changes have been made for these yet - this is a decision record to act on. + +## 1. Mandatory `;` after constructs that already end in a closing keyword + +**Fails today:** + +``` +TYPE + FLOAT_ARRAY_STATISTICS : STRUCT + count : UDINT; + END_STRUCT +END_TYPE +``` + +**Root cause:** `data_types.bnf:132` + +``` +data_type_declaration -> 'TYPE' (type_declaration ';')+ 'END_TYPE' +``` + +and `st.bnf:102` + +``` +statement_list -> (statement? ';')+ +``` + +Both require a trailing `;` after *every* element, with no exception for one +that already ends in its own closing keyword (`END_STRUCT`, `END_IF`, +`END_FOR`, `END_WHILE`, `END_CASE`, ...). Two of the six corpus samples hit +this: the struct `TYPE` (`END_STRUCT` directly followed by `END_TYPE`, no +`;`) and `utTestReporter` (`END_IF` directly followed by the next `IF`, no +`;`). Neither is a corpus mistake - both source files consistently omit the +`;` in this position. + +**Recommended fix:** make the separator optional specifically after +block-terminated alternatives, rather than loosening the rule everywhere +(which would also silently accept `42 ;;;; 43` as a valid decimal +literal - not the intent). Concretely, split `statement` into "the +alternatives that end in their own keyword" (`selection_statement`, +`iteration_statement`, `case_statement`, ...) and "the ones that don't" +(`assignment_statement`, `subprogram_control_statement`, ...), and only +require `;` after the latter: + +``` +statement_list -> (_terminated_statement | _self_terminated_statement ';'?)* +``` + +(exact rule split needs a look at every `statement` alternative; sketch only) +Same idea for `type_declaration ';'` in `data_type_declaration` - only +`single_element_type_declaration`/`type_name_reference` (bare `identifier +[:= ...]`, no closing keyword of their own) strictly need the `;`; +`array_type_declaration`/`structure_type_declaration`/`string_type_declaration` +already end in `END_STRUCT` or a length suffix and could make it optional the +same way. + +**Rationale:** this matches both real-world samples without becoming lenient +about the cases where `;` is the *only* thing separating two statements +(e.g. two bare assignments) - dropping it there would be a real ambiguity +risk, not just a style relaxation. + +## 2. `enumerated_value` requires a mandatory `TypeName#` qualifier + +**Fails today:** + +``` +TYPE + BINARY_ENCODING : (BASE64, BASE64_URL); +END_TYPE +``` + +**Root cause:** `data_types.bnf:178` + +``` +enumerated_value -> ((identifier => enumerated_type_name) '#') identifier +``` + +The `identifier '#'` qualifier prefix is not wrapped in `(...)?` - it's +mandatory. That's backwards for the most common case: when *declaring* an +enum's own value list (`enumerated_specification -> '(' enumerated_value +(',' enumerated_value)* ')'`, `data_types.bnf:170`), the values are bare +identifiers - `TypeName#Value` qualification is for *using* an enum value +elsewhere (disambiguating which enum a shared value name belongs to), not for +defining the list in the first place. + +**Recommended fix:** + +``` +enumerated_value -> ((identifier => enumerated_type_name) '#')? identifier + ; +``` + +**Rationale:** every real enum declaration in the corpus (and every IEC +example this reviewer is aware of) writes the value list as bare +identifiers. Making the qualifier optional doesn't lose the ability to parse +`TypeName#Value` where it's meaningful (`enumerated_value` is also reachable +from `_type_name_reference_init`/`array_initial_element` etc., where +qualification matters more), it just stops rejecting the declaration form. + +## 3. `STRING[n]` not accepted as a general type name + +**Fails today:** + +``` +FUNCTION CONCAT3 : STRING[254] + VAR_INPUT + in1 : STRING[254]; + END_VAR + CONCAT3 := in1; +END_FUNCTION +``` + +(fails on the `: STRING[254]` return type specifically; `in1 : STRING[254]` +inside `VAR_INPUT` is fine) + +**Root cause:** `data_types.bnf:23` / `variables.bnf:220` + +``` +elementary_type_name -> numeric_type_name | date_type_name + | bit_string_type_name | /W?STRING/ | 'TIME' + ; + +single_byte_string_spec -> 'STRING' ('[' integer ']')? (':=' single_byte_character_string)? + ; +``` + +The `[length]` suffix only exists on `single_byte_string_spec` / +`double_byte_string_spec`, which are reachable from a `VAR` declaration's +`_var1_init_decl`/`located_var_spec_init`, but *not* from +`_function_type_name` (`pou.bnf:29`, `elementary_type_name | type_name`) or +`simple_specification` (`data_types.bnf:153`, `-> elementary_type_name` +alone). Anywhere a `STRING`/`WSTRING` needs a length and isn't going through +one of the two `*_string_spec` rules, it can't have one. + +**Recommended fix:** thread the optional length onto `elementary_type_name`'s +`/W?STRING/` alternative directly (or a wrapper rule used in its place), +since the type name and its length aren't really separable concepts: + +``` +elementary_type_name -> numeric_type_name | date_type_name + | bit_string_type_name + | /W?STRING/ ('[' integer ']')? + | 'TIME' + ; +``` + +then drop the now-redundant `('[' integer ']')?` from +`single_byte_string_spec`/`double_byte_string_spec`, which would just be +`elementary_type_name (':=' ...)?` at that point - worth checking whether +those two rules collapse into `simple_spec_init` entirely once this lands, +rather than leaving a parallel path. + +**Rationale:** a return type, an input, and a plain variable are all "a type +name" in the same sense; there's no reason the length suffix should only be +reachable from one of the three paths that all eventually mean "this is a +`STRING`". + +## 4. `_function_decls` is single-occurrence and has no `VAR_TEMP` + +**Fails today:** + +``` +FUNCTION STRING_STARTSWITH : BOOL + VAR_INPUT + in1 : STRING[254]; + END_VAR + VAR_TEMP + in1_len : INT; + END_VAR + STRING_STARTSWITH := TRUE; +END_FUNCTION +``` + +**Root cause:** `pou.bnf:19,33` + +``` +function_declaration -> 'FUNCTION' derived_function_name ':' _function_type_name? _function_decls function_body 'END_FUNCTION' + ; + +_function_decls -> io_var_declarations + | function_var_decls + ; +``` + +Two separate problems: (a) `_function_decls` appears exactly once in +`function_declaration` - a function can have only *one* declarations block +total, when `VAR_INPUT`, `VAR_OUTPUT`, `VAR_IN_OUT`, and a temp/local block +are all ordinarily present together; (b) `function_var_decls -> 'VAR' +'CONSTANT'? ...` covers `VAR [CONSTANT] ... END_VAR`, but there's no +alternative covering `VAR_TEMP ... END_VAR` for functions - `temp_var_decls` +(`pou.bnf:109`) exists but is only wired into `other_var_declarations`, which +`_program_decls` (programs) uses, not `_function_decls` (functions). + +**Recommended fix:** + +``` +_function_decls -> (io_var_declarations | function_var_decls | temp_var_decls)* + ; +``` + +**Rationale:** matches how `_program_decls`/`other_var_declarations` +already handle repeated, mixed declaration blocks for `PROGRAM` and +`FUNCTION_BLOCK` - `FUNCTION` was the odd one out. Also brings the grammar in +line with the standard's own `+`/`*` cardinality on `io_var_declarations` in +the formal function-declaration production (a function needs at least one +input in practice, even though nothing here enforces that semantically - see +the existing `FIXME: NOTE 1` comment at `pou.bnf:57`). + +## Suggested order + +(2) and (4) are narrow, low-risk, single-rule changes. (3) touches a +lexical-ish rule used in a few places, worth a `make test` pass after. (1) is +the most involved - it changes cardinality/structure rather than adding an +alternative, and is worth doing last so the corpus can catch any fallout from +the other three first. diff --git a/tests/SOURCES.md b/tests/SOURCES.md new file mode 100644 index 0000000..2fe00fb --- /dev/null +++ b/tests/SOURCES.md @@ -0,0 +1,48 @@ +# Provenance of tests/community_samples.txt + +The test cases in `community_samples.txt` are small, complete excerpts of real, +human-written Structured Text taken from permissively-licensed open-source +repositories (not AI-generated), reformatted only to remove indentation that +came from a vendor-specific module wrapper the original files were nested in +(see below). Each is reproduced under the terms of the source project's MIT +license, which permits copying and redistribution provided the copyright +notice is retained - reproduced here: + +## WengerAG/structured-text-utilities + + - MIT License, +Copyright (c) 2019 Wenger Automation & Engineering AG, Winterthur, Switzerland. + +Used for: "Enumerated type", "Structure type", "Function with nested call +expression", "Function with VAR_TEMP and IF/ELSE", "Function referencing a +derived type and calling another function". + +Source files: `UTILITIES_BYTE.st`, `UTILITIES_MATH.st`, `UTILITIES_STRING.st`, +`UTILITIES_TIME.st`. In the original repository these declarations live inside +a B&R Automation Studio `UNIT ... INTERFACE ... IMPLEMENTATION ... END_UNIT` +module wrapper, which is a vendor-specific packaging construct, not part of +IEC 61131-3 itself; only the leading indentation from that wrapper was +stripped when extracting each declaration as a standalone top-level +`library_element_declaration`. + +## tkucic/UniTest + + - MIT License, Copyright (c) 2021 Toni Kucic. + +Used for: "Function block with FOR loop, array indexing and struct field +access" (`UniTest_br/UniTest/utTestReporter.st`). + +Note: two other files from this repository (`assertEqual_BOOL.st`, a +`FUNCTION`, and `Library_tests/main/main.st`, a `PROGRAM`) were considered but +dropped - in B&R Automation Studio projects the signature/`VAR` block of a POU +is stored in a separate companion file the IDE manages, so on their own these +particular files are implementation-only and don't parse as complete, +standalone declarations. They were not "fixed" with invented `VAR` blocks, +since that would stop being real source. + +## Coverage gap + +No standalone `PROGRAM` or `CONFIGURATION` declaration is included - these +tend to be project-specific and IDE-scaffolded rather than published in +shared, reusable libraries, so they were hard to find as complete, hand-written, +permissively-licensed single-file examples. diff --git a/tests/community_samples.txt b/tests/community_samples.txt new file mode 100644 index 0000000..f8292f8 --- /dev/null +++ b/tests/community_samples.txt @@ -0,0 +1,133 @@ +================== +Enumerated type (WengerAG structured-text-utilities) +================== + +TYPE + BINARY_ENCODING : ( + BASE64, // Base 64 Encoding as specified by RFC 4648 + BASE64_URL // Base 64 URL Encoding as specified by RFC 4648 + ); +END_TYPE + +--- + +================== +Structure type (WengerAG structured-text-utilities) +================== + +TYPE + // Contains statistics information of an array of floating values. + FLOAT_ARRAY_STATISTICS : STRUCT + count : UDINT; // count of values + sum : LREAL; // sum of all values + min_val : LREAL; // minimal value + max_val : LREAL; // maximal value + med_val : LREAL; // median value + avg_val : LREAL; // average value + sum_sqr : LREAL; // sum of squares + std_dev : LREAL; // standard deviation + END_STRUCT +END_TYPE + +--- + +================== +Function with nested call expression (WengerAG structured-text-utilities) +================== + +FUNCTION CONCAT3 : STRING[254] + VAR_INPUT + in1 : STRING[254]; + in2 : STRING[254]; + in3 : STRING[254]; + END_VAR + + CONCAT3 := CONCAT(CONCAT(in1, in2), in3); +END_FUNCTION + +--- + +================== +Function with VAR_TEMP and IF/ELSE (WengerAG structured-text-utilities) +================== + +FUNCTION STRING_STARTSWITH : BOOL + VAR_INPUT + in1 : STRING[254]; + in2 : STRING[254]; + END_VAR + + VAR_TEMP + in1_len : INT; + in2_len : INT; + END_VAR + + in1_len := LEN(in1); + in2_len := LEN(in2); + + IF in1_len < in2_len THEN + STRING_STARTSWITH := FALSE; + ELSE + STRING_STARTSWITH := LEFT(in1, in2_len) = in2; + END_IF; +END_FUNCTION + +--- + +================== +Function referencing a derived type and calling another function (WengerAG structured-text-utilities) +================== + +FUNCTION GET_DAY_OF_WEEK : USINT + VAR_INPUT + in : DATE; + kind : DAY_OF_WEEK_TYPE; // currently, only ISO 8601 is supported + END_VAR + + GET_DAY_OF_WEEK := GET_DAY_OF_WEEK_INTERNAL(DATE_TO_UDINT_INTERNAL(in)); +END_FUNCTION + +--- + +================== +Function block with FOR loop, array indexing and struct field access (tkucic/UniTest) +================== + +FUNCTION_BLOCK utTestReporter + (*Reset totals*) + NrPousUnderTest := 0; + NrOfTests := 0; + NrTestsPassed := 0; + NrTestsFailed := 0; + PassRate := 0; + TestsInProgress := FALSE; + Error := FALSE; + + (*Count totals*) + FOR i:=0 TO Size DO + IF Results[i].Id > 0 THEN + (*Count existing POUs under test*) + NrPousUnderTest := NrPousUnderTest + 1; + (*Count the pass rate. Needs to be divided in the end by the number of total tests*) + PassRate := PassRate + Results[i].PassRate; + (*Count the number of tests*) + NrOfTests := NrOfTests + Results[i].TotalTests; + (*Count the number of tests passed*) + NrTestsPassed := NrTestsPassed + Results[i].TestsPassed; + (*Count the number of tests failed*) + NrTestsFailed := NrTestsFailed + Results[i].TestsFailed; + + (*Indicators*) + IF Results[i].TestsRunning THEN + TestsInProgress := TRUE; + END_IF + IF Results[i].Error <> ut_NO_ERROR THEN + Error := TRUE; + END_IF + END_IF + END_FOR + PassRate := PassRate / NrPousUnderTest; +END_FUNCTION_BLOCK + +--- +