From cbe013fd4a5611216178302cfd8bc1bed8ad4150 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Fri, 23 May 2025 14:07:15 +0200 Subject: [PATCH 01/17] small fixes in grammar and collect comments from implementations --- blark/iec.lark | 5 +++-- blark/summary.py | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/blark/iec.lark b/blark/iec.lark index 90808d5..2f7c1dc 100644 --- a/blark/iec.lark +++ b/blark/iec.lark @@ -346,7 +346,7 @@ _array_initial_element: expression | enumerated_value | array_initialization -structure_type_declaration: structure_type_name_declaration [ extends ] ":" [ indirection_type ] _STRUCT ( structure_element_declaration ";"+ )* _END_STRUCT +structure_type_declaration: structure_type_name_declaration [ extends ] ":" [ indirection_type ] _STRUCT ";"* ( structure_element_declaration ";"+ )* _END_STRUCT initialized_structure: structure_type_name ":=" structure_initialization @@ -586,6 +586,7 @@ END_FUNCTION_BLOCK: _END_FUNCTION_BLOCK | input_output_declarations | external_var_declarations | var_declarations + | var_inst_declaration | temp_var_decls | static_var_declarations | incomplete_located_var_declarations @@ -608,7 +609,7 @@ function_block_method_declaration: _METHOD [ access_specifier ] DOTTED_IDENTIFIE ?property_return_type: _located_var_spec_init -function_block_property_declaration: _PROPERTY [ access_specifier ] DOTTED_IDENTIFIER [ ":" property_return_type ] ";"* property_var_declaration* [ function_block_body ] _END_PROPERTY ";"* +function_block_property_declaration: _PROPERTY [ access_specifier ] DOTTED_IDENTIFIER [ ":" property_return_type ] ";"* [ access_specifier ] property_var_declaration* [ function_block_body ] _END_PROPERTY ";"* // B.1.5.3 ?program_type_name: IDENTIFIER diff --git a/blark/summary.py b/blark/summary.py index b66dace..e6bf291 100644 --- a/blark/summary.py +++ b/blark/summary.py @@ -528,6 +528,7 @@ class FunctionBlockSummary(Summary): def __getitem__( self, key: str ) -> Union[DeclarationSummary, MethodSummary, PropertySummary, ActionSummary]: + key = key.strip(":;") if key in self.declarations: return self.declarations[key] for item in self.actions + self.methods + self.properties: @@ -1186,6 +1187,10 @@ def add_implementation(parsed: ParseResult, impl: tf.StatementList): match.implementation = impl + match.meta.comments.extend(parsed.comments) + # todo: inject parsed.source_code into match.meta for handling noqa: comments + # that way, we can check from the statements whether a noqa comment was found + context = [] def clear_context(): From 35f62cb773b85416ec2e50607da47823a0e688bf Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Fri, 23 May 2025 14:51:36 +0200 Subject: [PATCH 02/17] ignore keywords in identifier parsing --- blark/iec.lark | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blark/iec.lark b/blark/iec.lark index 2f7c1dc..8ba70a9 100644 --- a/blark/iec.lark +++ b/blark/iec.lark @@ -33,7 +33,7 @@ _library_element_declaration: data_type_declaration | ";" // B.1.1 -IDENTIFIER: /[A-Za-z_][A-Za-z0-9_]*/i +IDENTIFIER: /\b(?!(ABSTRACT|ACTION|AND|AND_THEN|ANY|ANY_BIT|ANY_DATE|ANY_DERIVED|ANY_ELEMENTARY|ANY_INT|ANY_MAGNITUDE|ANY_NUM|ANY_REAL|ANY_STRING|ARRAY|AT|BOOL|BY|BYTE|CASE|CONTINUE|DATE|DATE_AND_TIME|DINT|DO|DT|DWORD|ELSE|ELSIF|END_ACTION|END_CASE|END_FOR|END_FUNCTION|END_FUNCTIONBLOCK|END_FUNCTION_BLOCK|END_IF|END_INTERFACE|END_METHOD|END_PROGRAM|END_PROPERTY|END_REPEAT|END_STRUCT|END_TYPE|END_UNION|END_VAR|END_WHILE|EXIT|EXTENDS|FINAL|FOR|FUNCTION|FUNCTIONBLOCK|FUNCTION_BLOCK|IF|IMPLEMENTS|INT|INTERFACE|INTERNAL|JMP|LDATE|LDATE_AND_TIME|LDT|LINT|LREAL|LTIME|LTIME_OF_DAY|LTOD|LWORD|METHOD|MOD|NOT|OF|OR|OR_ELSE|PERSISTENT|POINTER|PRIVATE|PROGRAM|PROPERTY|PROTECTED|PUBLIC|READ_ONLY|READ_WRITE|REAL|REFERENCE|REPEAT|RETURN|SINT|STRUCT|THEN|TIME|TIME_OF_DAY|TO|TOD|TYPE|UDINT|UINT|ULINT|UNION|UNTIL|USINT|VAR|VAR_ACCESS|VAR_EXTERNAL|VAR_GLOBAL|VAR_INPUT|VAR_INST|VAR_IN_OUT|VAR_OUTPUT|VAR_STAT|VAR_TEMP|WHILE|WORD|XOR)\b)[A-Za-z_][A-Za-z0-9_]*\b/i // B.1.2 constant: time_literal From 9b63097d4445e0920cc9b771444eb6cf7fc561a4 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Fri, 23 May 2025 14:57:50 +0200 Subject: [PATCH 03/17] type names may be used as identifiers --- blark/iec.lark | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/blark/iec.lark b/blark/iec.lark index 8ba70a9..a1fc90b 100644 --- a/blark/iec.lark +++ b/blark/iec.lark @@ -33,7 +33,9 @@ _library_element_declaration: data_type_declaration | ";" // B.1.1 -IDENTIFIER: /\b(?!(ABSTRACT|ACTION|AND|AND_THEN|ANY|ANY_BIT|ANY_DATE|ANY_DERIVED|ANY_ELEMENTARY|ANY_INT|ANY_MAGNITUDE|ANY_NUM|ANY_REAL|ANY_STRING|ARRAY|AT|BOOL|BY|BYTE|CASE|CONTINUE|DATE|DATE_AND_TIME|DINT|DO|DT|DWORD|ELSE|ELSIF|END_ACTION|END_CASE|END_FOR|END_FUNCTION|END_FUNCTIONBLOCK|END_FUNCTION_BLOCK|END_IF|END_INTERFACE|END_METHOD|END_PROGRAM|END_PROPERTY|END_REPEAT|END_STRUCT|END_TYPE|END_UNION|END_VAR|END_WHILE|EXIT|EXTENDS|FINAL|FOR|FUNCTION|FUNCTIONBLOCK|FUNCTION_BLOCK|IF|IMPLEMENTS|INT|INTERFACE|INTERNAL|JMP|LDATE|LDATE_AND_TIME|LDT|LINT|LREAL|LTIME|LTIME_OF_DAY|LTOD|LWORD|METHOD|MOD|NOT|OF|OR|OR_ELSE|PERSISTENT|POINTER|PRIVATE|PROGRAM|PROPERTY|PROTECTED|PUBLIC|READ_ONLY|READ_WRITE|REAL|REFERENCE|REPEAT|RETURN|SINT|STRUCT|THEN|TIME|TIME_OF_DAY|TO|TOD|TYPE|UDINT|UINT|ULINT|UNION|UNTIL|USINT|VAR|VAR_ACCESS|VAR_EXTERNAL|VAR_GLOBAL|VAR_INPUT|VAR_INST|VAR_IN_OUT|VAR_OUTPUT|VAR_STAT|VAR_TEMP|WHILE|WORD|XOR)\b)[A-Za-z_][A-Za-z0-9_]*\b/i +// Identifiers need to ignore certain keywords, but NOT typenames, as those may be used +// 'as values' with SIZEOF for example +IDENTIFIER: /\b(?!(ABSTRACT|ACTION|AND|AND_THEN|ANY|ANY_BIT|ANY_DATE|ANY_DERIVED|ANY_ELEMENTARY|ANY_INT|ANY_MAGNITUDE|ANY_NUM|ANY_REAL|ANY_STRING|ARRAY|AT|BY|CASE|CONTINUE|DO|DT|ELSE|ELSIF|END_ACTION|END_CASE|END_FOR|END_FUNCTION|END_FUNCTIONBLOCK|END_FUNCTION_BLOCK|END_IF|END_INTERFACE|END_METHOD|END_PROGRAM|END_PROPERTY|END_REPEAT|END_STRUCT|END_TYPE|END_UNION|END_VAR|END_WHILE|EXIT|EXTENDS|FINAL|FOR|FUNCTION|FUNCTIONBLOCK|FUNCTION_BLOCK|IF|IMPLEMENTS|INTERFACE|INTERNAL|JMP|METHOD|MOD|NOT|OF|OR|OR_ELSE|PERSISTENT|PRIVATE|PROGRAM|PROPERTY|PROTECTED|PUBLIC|READ_ONLY|READ_WRITE|REFERENCE|REPEAT|RETURN|STRUCT|THEN|TO|TYPE|UNION|UNTIL|VAR|VAR_ACCESS|VAR_EXTERNAL|VAR_GLOBAL|VAR_INPUT|VAR_INST|VAR_IN_OUT|VAR_OUTPUT|VAR_STAT|VAR_TEMP|WHILE|XOR)\b)[A-Za-z_][A-Za-z0-9_]*\b/i // B.1.2 constant: time_literal From d7528b1948f2c983eca3d12272b70f852e74af19 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Fri, 23 May 2025 16:08:03 +0200 Subject: [PATCH 04/17] allow empty var declaration bodies --- blark/iec.lark | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blark/iec.lark b/blark/iec.lark index a1fc90b..9d98fed 100644 --- a/blark/iec.lark +++ b/blark/iec.lark @@ -35,7 +35,7 @@ _library_element_declaration: data_type_declaration // B.1.1 // Identifiers need to ignore certain keywords, but NOT typenames, as those may be used // 'as values' with SIZEOF for example -IDENTIFIER: /\b(?!(ABSTRACT|ACTION|AND|AND_THEN|ANY|ANY_BIT|ANY_DATE|ANY_DERIVED|ANY_ELEMENTARY|ANY_INT|ANY_MAGNITUDE|ANY_NUM|ANY_REAL|ANY_STRING|ARRAY|AT|BY|CASE|CONTINUE|DO|DT|ELSE|ELSIF|END_ACTION|END_CASE|END_FOR|END_FUNCTION|END_FUNCTIONBLOCK|END_FUNCTION_BLOCK|END_IF|END_INTERFACE|END_METHOD|END_PROGRAM|END_PROPERTY|END_REPEAT|END_STRUCT|END_TYPE|END_UNION|END_VAR|END_WHILE|EXIT|EXTENDS|FINAL|FOR|FUNCTION|FUNCTIONBLOCK|FUNCTION_BLOCK|IF|IMPLEMENTS|INTERFACE|INTERNAL|JMP|METHOD|MOD|NOT|OF|OR|OR_ELSE|PERSISTENT|PRIVATE|PROGRAM|PROPERTY|PROTECTED|PUBLIC|READ_ONLY|READ_WRITE|REFERENCE|REPEAT|RETURN|STRUCT|THEN|TO|TYPE|UNION|UNTIL|VAR|VAR_ACCESS|VAR_EXTERNAL|VAR_GLOBAL|VAR_INPUT|VAR_INST|VAR_IN_OUT|VAR_OUTPUT|VAR_STAT|VAR_TEMP|WHILE|XOR)\b)[A-Za-z_][A-Za-z0-9_]*\b/i +IDENTIFIER: /\b(?!(ABSTRACT|ACTION|AND|AND_THEN|AT|BY|CASE|CONTINUE|DO|ELSE|ELSIF|END_ACTION|END_CASE|END_FOR|END_FUNCTION|END_FUNCTIONBLOCK|END_FUNCTION_BLOCK|END_IF|END_INTERFACE|END_METHOD|END_PROGRAM|END_PROPERTY|END_REPEAT|END_STRUCT|END_TYPE|END_UNION|END_VAR|END_WHILE|EXIT|EXTENDS|FINAL|FOR|FUNCTION|FUNCTIONBLOCK|FUNCTION_BLOCK|IF|IMPLEMENTS|INTERFACE|INTERNAL|JMP|METHOD|MOD|NOT|OF|OR|OR_ELSE|PERSISTENT|PRIVATE|PROGRAM|PROPERTY|PROTECTED|PUBLIC|READ_ONLY|READ_WRITE|REFERENCE|REPEAT|RETURN|STRUCT|THEN|TO|TYPE|UNION|UNTIL|VAR|VAR_ACCESS|VAR_EXTERNAL|VAR_GLOBAL|VAR_INPUT|VAR_INST|VAR_IN_OUT|VAR_OUTPUT|VAR_STAT|VAR_TEMP|WHILE|XOR)\b)[A-Za-z_][A-Za-z0-9_]*\b/i // B.1.2 constant: time_literal @@ -448,7 +448,7 @@ fb_decl: fb_decl_name_list ":" function_block_type_name [ ":=" structure_initial fb_decl_name_list: fb_name ( "," fb_name )* -var_body: ( var_init_decl ";"+ )* +var_body: ";"* ( var_init_decl ";"+ )* array_var_declaration: var1_list ":" array_specification From 415739d73cfc066ab937ce5040de5e6a011ca3d4 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Tue, 27 May 2025 13:03:28 +0200 Subject: [PATCH 05/17] use dict objects instead of lxml Attrs for metadata --- blark/solution.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blark/solution.py b/blark/solution.py index 6921938..174ad53 100644 --- a/blark/solution.py +++ b/blark/solution.py @@ -366,7 +366,7 @@ def from_xml( implementation=get_child_located_text( xml, "Implementation/ST", filename=filename ), - metadata=xml.attrib, + metadata=dict(xml.attrib), source_type=source_type, filename=filename, ) @@ -1020,7 +1020,7 @@ def from_xml( xml: lxml.etree.Element, parent: TcSource, ) -> Self: - return cls(metadata=xml.attrib, xml=xml, parent=parent) + return cls(metadata=dict(xml.attrib), xml=xml, parent=parent) @dataclasses.dataclass From 2e9509c29a945998166bc0e7bb92c6c8ab07332b Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Fri, 20 Jun 2025 10:35:32 +0200 Subject: [PATCH 06/17] convert tokens to strings in summary dicts and inject implementation source to summaries --- blark/summary.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/blark/summary.py b/blark/summary.py index e6bf291..43e720a 100644 --- a/blark/summary.py +++ b/blark/summary.py @@ -188,7 +188,7 @@ def from_declaration( **Summary.get_meta_kwargs(item.meta), ) elif isinstance(item, tf.UnionElementDeclaration): - result[item.name] = DeclarationSummary( + result[str(item.name)] = DeclarationSummary( name=str(item.name), item=item, location=None, @@ -239,7 +239,7 @@ def from_declaration( init = str(getattr(item.spec, "init", None)) type_ = getattr(init, "full_type_name", str(item.spec)) base_type = getattr(init, "base_type_name", str(item.spec)) - result[item.name] = DeclarationSummary( + result[str(item.name)] = DeclarationSummary( name=str(item.name), item=item, location=location, @@ -310,6 +310,7 @@ class ActionSummary(Summary): item: tf.Action source_code: str implementation: Optional[tf.StatementList] = None + implementation_source: Optional[str] = None def __getitem__(self, key: str) -> None: raise KeyError(f"{key}: Actions do not contain declarations") @@ -365,6 +366,7 @@ class MethodSummary(Summary): return_type: Optional[str] source_code: str implementation: Optional[tf.StatementList] = None + implementation_source: Optional[str] = None declarations: Dict[str, DeclarationSummary] = field(default_factory=dict) def __getitem__(self, key: str) -> DeclarationSummary: @@ -410,6 +412,7 @@ class PropertyGetSetSummary(Summary): source_code: str declarations: Dict[str, DeclarationSummary] = field(default_factory=dict) implementation: Optional[tf.StatementList] = None + implementation_source: Optional[str] = None def __getitem__(self, key: str) -> DeclarationSummary: return self.declarations[key] @@ -472,6 +475,7 @@ class FunctionSummary(Summary): return_type: Optional[str] source_code: str implementation: Optional[tf.StatementList] = None + implementation_source: Optional[str] = None declarations: Dict[str, DeclarationSummary] = field(default_factory=dict) def __getitem__(self, key: str) -> DeclarationSummary: @@ -520,6 +524,7 @@ class FunctionBlockSummary(Summary): extends: Optional[str] squashed: bool implementation: Optional[tf.StatementList] = None + implementation_source: Optional[str] = None declarations: Dict[str, DeclarationSummary] = field(default_factory=dict) actions: List[ActionSummary] = field(default_factory=list) methods: List[MethodSummary] = field(default_factory=list) @@ -861,6 +866,7 @@ class ProgramSummary(Summary): source_code: str item: tf.Program implementation: Optional[tf.StatementList] = None + implementation_source: Optional[str] = None declarations: Dict[str, DeclarationSummary] = field(default_factory=dict) actions: List[ActionSummary] = field(default_factory=list) methods: List[MethodSummary] = field(default_factory=list) @@ -1188,8 +1194,7 @@ def add_implementation(parsed: ParseResult, impl: tf.StatementList): match.implementation = impl match.meta.comments.extend(parsed.comments) - # todo: inject parsed.source_code into match.meta for handling noqa: comments - # that way, we can check from the statements whether a noqa comment was found + match.implementation_source = parsed.source_code context = [] @@ -1227,7 +1232,7 @@ def get_pou_context() -> Union[ source_code=get_code_by_meta(parsed, item.meta), filename=parsed.filename, ) - result.function_blocks[item.name] = summary + result.function_blocks[str(item.name)] = summary new_context(summary) elif isinstance(item, tf.Function): summary = FunctionSummary.from_function( @@ -1235,7 +1240,7 @@ def get_pou_context() -> Union[ source_code=get_code_by_meta(parsed, item.meta), filename=parsed.filename, ) - result.functions[item.name] = summary + result.functions[str(item.name)] = summary new_context(summary) elif isinstance(item, tf.DataTypeDeclaration): if isinstance( @@ -1283,7 +1288,7 @@ def get_pou_context() -> Union[ source_code=get_code_by_meta(parsed, item.meta), filename=parsed.filename, ) - result.globals[item.name] = summary + result.globals[str(item.name)] = summary # for global_var in summary.declarations.values(): # if not qualified_only: # result.globals[global_var.name] = summary @@ -1295,7 +1300,7 @@ def get_pou_context() -> Union[ source_code=get_code_by_meta(parsed, item.meta), filename=parsed.filename, ) - result.programs[item.name] = summary + result.programs[str(item.name)] = summary new_context(summary) elif isinstance(item, tf.Interface): summary = InterfaceSummary.from_interface( @@ -1303,7 +1308,7 @@ def get_pou_context() -> Union[ source_code=get_code_by_meta(parsed, item.meta), filename=parsed.filename, ) - result.interfaces[item.name] = summary + result.interfaces[str(item.name)] = summary new_context(summary) elif isinstance(item, tf.StatementList): if parsed.item.type != SourceType.action: From dd412de6a2c9ca3e54ef3e9f455883d2151a6284 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Fri, 20 Jun 2025 11:32:13 +0200 Subject: [PATCH 07/17] add declarations to property getter / setter summaries and properly handle updated access specifier location grammar --- blark/summary.py | 13 ++++++++++--- blark/transform.py | 3 ++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/blark/summary.py b/blark/summary.py index 43e720a..9197239 100644 --- a/blark/summary.py +++ b/blark/summary.py @@ -166,7 +166,7 @@ def from_declaration( cls, item: Union[tf.InitDeclaration, tf.StructureElementDeclaration, tf.UnionElementDeclaration], parent: Optional[ - Union[tf.Function, tf.Method, tf.FunctionBlock, tf.StructureTypeDeclaration] + Union[tf.Function, tf.Method, tf.FunctionBlock, tf.Property, tf.StructureTypeDeclaration] ] = None, block_header: str = "unknown", filename: Optional[pathlib.Path] = None, @@ -287,7 +287,7 @@ def from_global_variable( def from_block( cls, block: tf.VariableDeclarationBlock, - parent: Union[tf.Function, tf.Method, tf.FunctionBlock], + parent: Union[tf.Function, tf.Method, tf.FunctionBlock, tf.Property], filename: Optional[pathlib.Path] = None, ) -> Dict[str, DeclarationSummary]: result = {} @@ -445,7 +445,7 @@ def from_property( source_code = str(property) # TODO: this is broken at the moment - return PropertySummary( + summary = PropertySummary( name=str(property.name), getter=PropertyGetSetSummary( name=str(property.name), @@ -466,6 +466,13 @@ def from_property( **Summary.get_meta_kwargs(property.meta), ) + for decl in property.declarations: + decl_summary = DeclarationSummary.from_block(decl, parent=property, filename=filename) + summary.getter.declarations.update(decl_summary) + summary.setter.declarations.update(decl_summary) + + return summary + @dataclass class FunctionSummary(Summary): diff --git a/blark/transform.py b/blark/transform.py index df7b5c5..c985d38 100644 --- a/blark/transform.py +++ b/blark/transform.py @@ -3266,12 +3266,13 @@ def from_lark( access: Optional[AccessSpecifier], name: lark.Token, return_type: Optional[LocatedVariableSpecInit], + access_: Optional[AccessSpecifier], *args ) -> Property: *declarations, body = args return Property( name=name, - access=access, + access=access or access_, return_type=return_type, declarations=list(declarations), body=body, From 01d061187231d4692405b80cfe5ebe533833a6fc Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Tue, 1 Jul 2025 15:42:40 +0200 Subject: [PATCH 08/17] small fixes (END_INTERFACE opt, multiple extends) --- blark/iec.lark | 4 +- blark/summary.py | 110 ++++++++++++++++++++++++++++----------------- blark/transform.py | 11 ++++- 3 files changed, 80 insertions(+), 45 deletions(-) diff --git a/blark/iec.lark b/blark/iec.lark index 9d98fed..2a73004 100644 --- a/blark/iec.lark +++ b/blark/iec.lark @@ -572,7 +572,7 @@ ACCESS_SPECIFIER: _ABSTRACT | _INTERNAL | _FINAL access_specifier: ACCESS_SPECIFIER+ -extends: _EXTENDS DOTTED_IDENTIFIER +extends: _EXTENDS DOTTED_IDENTIFIER ("," DOTTED_IDENTIFIER)* implements: _IMPLEMENTS DOTTED_IDENTIFIER ("," DOTTED_IDENTIFIER)* function_block_type_declaration: FUNCTION_BLOCK [ access_specifier ] derived_function_block_name [ extends ] [ implements ] fb_var_declaration* [ function_block_body ] END_FUNCTION_BLOCK ";"* @@ -649,7 +649,7 @@ program_access_decl: access_name ":" symbolic_variable ":" non_generic_type_name | external_var_declarations | var_declarations -interface_declaration: _INTERFACE IDENTIFIER [ extends ] interface_var_declaration* _END_INTERFACE ";"* +interface_declaration: _INTERFACE IDENTIFIER [ extends ] interface_var_declaration* _END_INTERFACE? ";"* // B.2.1, B.3.1 LOGICAL_OR: _OR diff --git a/blark/summary.py b/blark/summary.py index 9197239..c15e120 100644 --- a/blark/summary.py +++ b/blark/summary.py @@ -528,7 +528,7 @@ class FunctionBlockSummary(Summary): name: str source_code: str item: tf.FunctionBlock - extends: Optional[str] + extends: Optional[list[str]] squashed: bool implementation: Optional[tf.StatementList] = None implementation_source: Optional[str] = None @@ -570,7 +570,7 @@ def from_function_block( item=fb, source_code=source_code, filename=filename, - extends=fb.extends.name if fb.extends else None, + extends=fb.extends.interfaces if fb.extends else None, squashed=False, **Summary.get_meta_kwargs(fb.meta), ) @@ -582,32 +582,42 @@ def from_function_block( return summary + def squash_base_extends( self, function_blocks: Dict[str, FunctionBlockSummary] ) -> FunctionBlockSummary: """Squash the "EXTENDS" function block into this one.""" - if self.extends is None: + if not self.extends: return self - extends_from = function_blocks.get(str(self.extends), None) - if extends_from is None: + extends_from: list[FunctionBlockSummary] = [ + function_blocks.get(str(ext), None) + for ext in self.extends + ] + extends_from = [ + fb.squash_base_extends(function_blocks) + for fb in extends_from + if fb is not None + ] + if not extends_from: return self - if extends_from.extends: - extends_from = extends_from.squash_base_extends(function_blocks) - - declarations = dict(extends_from.declarations) + declarations = {} + for ext in extends_from: + declarations.update(ext.declarations) declarations.update(self.declarations) - actions = list(extends_from.actions) + self.actions - methods = list(extends_from.methods) + self.methods - properties = list(extends_from.properties) + self.properties + actions = [a for fb in extends_from for a in fb.actions] + self.actions + methods = [m for fb in extends_from for m in fb.methods] + self.methods + properties = [p for fb in extends_from for p in fb.properties] + self.properties return FunctionBlockSummary( name=self.name, - comments=extends_from.comments + self.comments, - pragmas=extends_from.pragmas + self.pragmas, + comments=[c for fb in extends_from for c in fb.comments] + self.comments, + pragmas=[p for fb in extends_from for p in fb.pragmas] + self.pragmas, meta=self.meta, filename=self.filename, - source_code="\n\n".join((extends_from.source_code, self.source_code)), + source_code="\n\n".join( + (*(ext.source_code for ext in extends_from), self.source_code) + ), item=self.item, extends=self.extends, declarations=declarations, @@ -625,7 +635,7 @@ class InterfaceSummary(Summary): name: str source_code: str item: tf.Interface - extends: Optional[str] + extends: Optional[list[str]] squashed: bool declarations: Dict[str, DeclarationSummary] = field(default_factory=dict) methods: List[MethodSummary] = field(default_factory=list) @@ -666,7 +676,7 @@ def from_interface( item=itf, source_code=source_code, filename=filename, - extends=itf.extends.name if itf.extends else None, + extends=itf.extends.interfaces if itf.extends else None, squashed=False, **Summary.get_meta_kwargs(itf.meta), ) @@ -682,27 +692,36 @@ def squash_base_extends( self, interfaces: Dict[str, InterfaceSummary] ) -> InterfaceSummary: """Squash the "EXTENDS" INTERFACE into this one.""" - if self.extends is None: + if not self.extends: return self - extends_from = interfaces.get(str(self.extends), None) - if extends_from is None: + extends_from: list[InterfaceSummary] = [ + interfaces.get(str(ext), None) + for ext in self.extends + ] + extends_from = [ + fb.squash_base_extends(interfaces) + for fb in extends_from + if fb is not None + ] + if not extends_from: return self - if extends_from.extends: - extends_from = extends_from.squash_base_extends(interfaces) - - declarations = dict(extends_from.declarations) + declarations = {} + for ext in extends_from: + declarations.update(ext.declarations) declarations.update(self.declarations) - methods = list(extends_from.methods) + self.methods - properties = list(extends_from.properties) + self.properties + methods = [m for fb in extends_from for m in fb.methods] + self.methods + properties = [p for fb in extends_from for p in fb.properties] + self.properties return InterfaceSummary( name=self.name, - comments=extends_from.comments + self.comments, - pragmas=extends_from.pragmas + self.pragmas, + comments=[c for fb in extends_from for c in fb.comments] + self.comments, + pragmas=[p for fb in extends_from for p in fb.pragmas] + self.pragmas, meta=self.meta, filename=self.filename, - source_code="\n\n".join((extends_from.source_code, self.source_code)), + source_code="\n\n".join( + (*(ext.source_code for ext in extends_from), self.source_code) + ), item=self.item, extends=self.extends, declarations=declarations, @@ -720,7 +739,7 @@ class DataTypeSummary(Summary): item: tf.TypeDeclarationItem source_code: str type: str - extends: Optional[str] + extends: Optional[list[str]] squashed: bool = False declarations: Dict[str, DeclarationSummary] = field(default_factory=dict) @@ -744,7 +763,7 @@ def from_data_type( source_code = str(dtype) if isinstance(dtype, tf.StructureTypeDeclaration): - extends = dtype.extends.name if dtype.extends else None + extends = dtype.extends.interfaces if dtype.extends else None else: extends = None @@ -787,26 +806,35 @@ def squash_base_extends( self, data_types: Dict[str, DataTypeSummary] ) -> DataTypeSummary: """Squash the "EXTENDS" function block into this one.""" - if self.extends is None: + if not self.extends: return self - extends_from = data_types.get(str(self.extends), None) - if extends_from is None: + extends_from: list[DataTypeSummary] = [ + data_types.get(str(ext), None) + for ext in self.extends + ] + extends_from = [ + fb.squash_base_extends(data_types) + for fb in extends_from + if fb is not None + ] + if not extends_from: return self - if extends_from.extends: - extends_from = extends_from.squash_base_extends(data_types) - - declarations = dict(extends_from.declarations) + declarations = {} + for ext in extends_from: + declarations.update(ext.declarations) declarations.update(self.declarations) return DataTypeSummary( name=self.name, type=self.type, - comments=extends_from.comments + self.comments, - pragmas=extends_from.pragmas + self.pragmas, + comments=[c for fb in extends_from for c in fb.comments] + self.comments, + pragmas=[p for fb in extends_from for p in fb.pragmas] + self.pragmas, meta=self.meta, filename=self.filename, - source_code="\n\n".join((extends_from.source_code, self.source_code)), + source_code="\n\n".join( + (*(ext.source_code for ext in extends_from), self.source_code) + ), item=self.item, extends=self.extends, declarations=declarations, diff --git a/blark/transform.py b/blark/transform.py index c985d38..58b06f8 100644 --- a/blark/transform.py +++ b/blark/transform.py @@ -2874,13 +2874,20 @@ class Extends: EXTENDS stName EXTENDS FB_Name + EXTENDS FB_Name, FB_Name2 """ - name: lark.Token + interfaces: List[lark.Token] meta: Optional[Meta] = meta_field() + @staticmethod + def from_lark( + *interfaces: lark.Token, + ) -> Extends: + return Extends(interfaces=list(interfaces)) + def __str__(self) -> str: - return f"EXTENDS {self.name}" + return "EXTENDS " + ", ".join(self.interfaces) @dataclass From 5f4e831715d38e6fe5ea2c545b4add7494a07aec Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Thu, 3 Jul 2025 13:31:56 +0200 Subject: [PATCH 09/17] properly consolidate property getters and setters in a single summary --- blark/solution.py | 10 +++++++--- blark/summary.py | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/blark/solution.py b/blark/solution.py index 174ad53..7479d2a 100644 --- a/blark/solution.py +++ b/blark/solution.py @@ -948,7 +948,11 @@ def to_blark(self) -> list[Union[BlarkCompositeSourceItem, BlarkSourceItem]]: property_ident = Identifier.from_string(base_decl.identifier) parts = [] - for get_or_set, obj in (("get", self.get), ("set", self.set)): + get_set = [ + ("get", SourceType.property_get, self.get), + ("set", SourceType.property_set, self.set), + ] + for get_or_set, source_type, obj in get_set: if obj is None: continue @@ -961,7 +965,7 @@ def to_blark(self) -> list[Union[BlarkCompositeSourceItem, BlarkSourceItem]]: parts=[*property_ident.parts, get_or_set], decl_impl="declaration", ).to_string(), - type=SourceType.property, + type=source_type, lines=base_decl.lines + decl.lines, grammar_rule=SourceType.property.get_grammar_rule(), implicit_end="END_PROPERTY", @@ -977,7 +981,7 @@ def to_blark(self) -> list[Union[BlarkCompositeSourceItem, BlarkSourceItem]]: parts=[*property_ident.parts, get_or_set], decl_impl="implementation", ).to_string(), - type=SourceType.property, + type=source_type, lines=impl.lines, grammar_rule=SourceType.statement_list.get_grammar_rule(), implicit_end="", diff --git a/blark/summary.py b/blark/summary.py index c15e120..ac580a4 100644 --- a/blark/summary.py +++ b/blark/summary.py @@ -1314,7 +1314,22 @@ def get_pou_context() -> Union[ source_code=get_code_by_meta(parsed, item.meta), filename=parsed.filename, ) - pou.properties.append(summary) + # property getters and setters are separate parse results, and must + # therefore be consolidated here + existing = next( + (prop for prop in pou.properties if prop.name == summary.name), + None + ) + if existing is not None: + if parsed.item.type == SourceType.property_get: + existing.getter = summary.getter + elif parsed.item.type == SourceType.property_set: + existing.setter = summary.setter + else: + raise TypeError(f"Unsupported property summary: {parsed.item.type}") + else: + # add new property + pou.properties.append(summary) push_context(summary) elif isinstance(item, tf.GlobalVariableDeclarations): clear_context() From a7317ea0172c764ec91ac0cfdc5ae5beb171e7ce Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Thu, 3 Jul 2025 14:05:41 +0200 Subject: [PATCH 10/17] add case-insensitive search for extensions --- blark/summary.py | 8 ++++---- blark/util.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/blark/summary.py b/blark/summary.py index ac580a4..43e5dfd 100644 --- a/blark/summary.py +++ b/blark/summary.py @@ -12,7 +12,7 @@ from . import transform as tf from .parse import ParseResult from .typing import Literal -from .util import Identifier, SourceType +from .util import Identifier, SourceType, get_case_insensitive LocationType = Literal["input", "output", "memory"] @@ -591,7 +591,7 @@ def squash_base_extends( return self extends_from: list[FunctionBlockSummary] = [ - function_blocks.get(str(ext), None) + get_case_insensitive(function_blocks, str(ext)) for ext in self.extends ] extends_from = [ @@ -696,7 +696,7 @@ def squash_base_extends( return self extends_from: list[InterfaceSummary] = [ - interfaces.get(str(ext), None) + get_case_insensitive(interfaces, str(ext)) for ext in self.extends ] extends_from = [ @@ -810,7 +810,7 @@ def squash_base_extends( return self extends_from: list[DataTypeSummary] = [ - data_types.get(str(ext), None) + get_case_insensitive(data_types, str(ext)) for ext in self.extends ] extends_from = [ diff --git a/blark/util.py b/blark/util.py index b4e6cca..4597928 100644 --- a/blark/util.py +++ b/blark/util.py @@ -128,6 +128,17 @@ def from_string(cls: type[Self], value: str) -> Self: ) +def get_case_insensitive(dct: dict[str, Any], key: str, default=None): + """Get case-insensitive key from a dictionary, with default. Useful because TwinCAT is + case-insensitive.""" + if key in dct: + return dct[key] + for k, v in dct.items(): + if k.lower() == key.lower(): + return v + return default + + def get_source_code(fn: AnyPath, *, encoding: str = "utf-8") -> str: """ Get source code from the given file. From 822fa0de067abc91116e1d2cbee5e453c19fccb0 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Tue, 8 Jul 2025 09:59:49 +0200 Subject: [PATCH 11/17] why not store all data types? --- blark/summary.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/blark/summary.py b/blark/summary.py index 43e5dfd..606c469 100644 --- a/blark/summary.py +++ b/blark/summary.py @@ -1278,16 +1278,12 @@ def get_pou_context() -> Union[ result.functions[str(item.name)] = summary new_context(summary) elif isinstance(item, tf.DataTypeDeclaration): - if isinstance( + summary = DataTypeSummary.from_data_type( item.declaration, - (tf.StructureTypeDeclaration, tf.UnionTypeDeclaration) - ): - summary = DataTypeSummary.from_data_type( - item.declaration, - source_code=get_code_by_meta(parsed, item.declaration.meta), - filename=parsed.filename, - ) - result.data_types[item.declaration.name] = summary + source_code=get_code_by_meta(parsed, item.declaration.meta), + filename=parsed.filename, + ) + result.data_types[item.declaration.name] = summary clear_context() elif isinstance(item, tf.Method): pou = get_pou_context() From f848d9e125992b3795c6beb2831dd19e04c03a67 Mon Sep 17 00:00:00 2001 From: Dennis Hilhorst Date: Thu, 10 Jul 2025 15:27:00 +0200 Subject: [PATCH 12/17] more case insensitivity and no more : or ; in item names --- blark/plain.py | 4 ++-- blark/solution.py | 4 +--- blark/summary.py | 41 +++++++++++++++++++++-------------------- blark/util.py | 27 +++++++++++++++++++++++++-- 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/blark/plain.py b/blark/plain.py index 0a14e09..911e587 100644 --- a/blark/plain.py +++ b/blark/plain.py @@ -8,7 +8,7 @@ from .input import (BlarkCompositeSourceItem, BlarkSourceItem, register_input_handler) from .output import OutputBlock, register_output_handler -from .util import AnyPath, SourceType, find_pou_type_and_identifier +from .util import AnyPath, SourceType, find_pou_type_and_identifier_plain @dataclasses.dataclass @@ -40,7 +40,7 @@ def load( with open(filename, "rt") as fp: contents = fp.read() - source_type, identifier = find_pou_type_and_identifier(contents) + source_type, identifier = find_pou_type_and_identifier_plain(contents) # if source_type is None: # return [] source_type = SourceType.general diff --git a/blark/solution.py b/blark/solution.py index 7479d2a..eb99256 100644 --- a/blark/solution.py +++ b/blark/solution.py @@ -351,9 +351,7 @@ def from_xml( ) -> Self: declaration = get_child_located_text(xml, "Declaration", filename=filename) if declaration is not None: - source_type, identifier = util.find_pou_type_and_identifier( - declaration.value - ) + source_type, identifier = util.find_pou_type_and_identifier_xml(xml) else: source_type, identifier = None, None diff --git a/blark/summary.py b/blark/summary.py index 606c469..315c753 100644 --- a/blark/summary.py +++ b/blark/summary.py @@ -370,7 +370,7 @@ class MethodSummary(Summary): declarations: Dict[str, DeclarationSummary] = field(default_factory=dict) def __getitem__(self, key: str) -> DeclarationSummary: - return self.declarations[key] + return get_case_insensitive(self.declarations, key) @property def declarations_by_block(self) -> Dict[str, Dict[str, DeclarationSummary]]: @@ -415,7 +415,7 @@ class PropertyGetSetSummary(Summary): implementation_source: Optional[str] = None def __getitem__(self, key: str) -> DeclarationSummary: - return self.declarations[key] + return get_case_insensitive(self.declarations, key) @dataclass @@ -486,7 +486,7 @@ class FunctionSummary(Summary): declarations: Dict[str, DeclarationSummary] = field(default_factory=dict) def __getitem__(self, key: str) -> DeclarationSummary: - return self.declarations[key] + return get_case_insensitive(self.declarations, key) @property def declarations_by_block(self) -> Dict[str, Dict[str, DeclarationSummary]]: @@ -540,11 +540,11 @@ class FunctionBlockSummary(Summary): def __getitem__( self, key: str ) -> Union[DeclarationSummary, MethodSummary, PropertySummary, ActionSummary]: - key = key.strip(":;") - if key in self.declarations: - return self.declarations[key] + decl = get_case_insensitive(self.declarations, key) + if decl is not None: + return decl for item in self.actions + self.methods + self.properties: - if item.name == key: + if item.name.lower() == key.lower(): return item raise KeyError(key) @@ -647,10 +647,11 @@ class InterfaceSummary(Summary): def __getitem__( self, key: str ) -> Union[DeclarationSummary, MethodSummary, PropertySummary]: - if key in self.declarations: - return self.declarations[key] - for item in self.methods + self.properties: - if item.name == key: + decl = get_case_insensitive(self.declarations, key) + if decl is not None: + return decl + for item in self.actions + self.methods + self.properties: + if item.name.lower() == key.lower(): return item raise KeyError(key) @@ -744,7 +745,7 @@ class DataTypeSummary(Summary): declarations: Dict[str, DeclarationSummary] = field(default_factory=dict) def __getitem__(self, key: str) -> DeclarationSummary: - return self.declarations[key] + return get_case_insensitive(self.declarations, key) @property def declarations_by_block(self) -> Dict[str, Dict[str, DeclarationSummary]]: @@ -853,7 +854,7 @@ class GlobalVariableSummary(Summary): declarations: Dict[str, DeclarationSummary] = field(default_factory=dict) def __getitem__(self, key: str) -> DeclarationSummary: - return self.declarations[key] + return get_case_insensitive(self.declarations, key) @property def declarations_by_block(self) -> Dict[str, Dict[str, DeclarationSummary]]: @@ -908,10 +909,11 @@ class ProgramSummary(Summary): properties: List[PropertySummary] = field(default_factory=list) def __getitem__(self, key: str) -> DeclarationSummary: - if key in self.declarations: - return self.declarations[key] + decl = get_case_insensitive(self.declarations, key) + if decl is not None: + return decl for item in self.actions + self.methods + self.properties: - if item.name == key: + if item.name.lower() == key.lower(): return item raise KeyError(key) @@ -1129,10 +1131,9 @@ def get_all_items_by_name( self.data_types, ): # Very inefficient, be warned - try: - yield dct[name] - except KeyError: - ... + decl = get_case_insensitive(dct, name) + if decl is not None: + yield decl def get_item_by_name(self, name: str) -> Optional[TopLevelCodeSummaryType]: """ diff --git a/blark/util.py b/blark/util.py index 4597928..2cc3087 100644 --- a/blark/util.py +++ b/blark/util.py @@ -224,11 +224,12 @@ def python_debug_session(namespace: Dict[str, Any], message: str): embed() -def find_pou_type_and_identifier(code: str) -> tuple[Optional[SourceType], Optional[str]]: +def find_pou_type_and_identifier_plain(code: str) -> tuple[Optional[SourceType], Optional[str]]: types = {source.name for source in SourceType} clean_code = remove_all_comments(code) for line in clean_code.splitlines(): - parts = line.lstrip().split() + # split line on non-word and non-dot characters + parts = re.split(r"[^\w.]+", line.lstrip()) if parts and parts[0].lower() in types: source_type = SourceType[parts[0].lower()] identifier = None @@ -247,6 +248,28 @@ def find_pou_type_and_identifier(code: str) -> tuple[Optional[SourceType], Optio return None, None +def find_pou_type_and_identifier_xml( + xml: lxml.etree.Element +) -> tuple[Optional[SourceType], Optional[str]]: + tag_source_type_map = { + "Get": SourceType.property_get, + "Set": SourceType.property_set, + "Itf": SourceType.interface, + "GVL": SourceType.var_global, + } + + if xml.tag in tag_source_type_map: + source_type = tag_source_type_map[xml.tag] + elif xml.tag == "POU": + # POU may be function block or function (or maybe others) + # so we figure it out the 'old fashioned' way + source_type, _ = find_pou_type_and_identifier_plain(xml.xpath("Declaration")[0].text) + else: + source_type = SourceType[xml.tag.lower()] + + return source_type, xml.attrib.get("Name") + + def remove_all_comments(text: str, *, replace_char: str = " ") -> str: """ Remove all comments and replace them with the provided character. From a7761028a962ada5ef5c80204ad1e29b70cc6698 Mon Sep 17 00:00:00 2001 From: Ken Lauer <5139267+klauer@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:57:37 -0700 Subject: [PATCH 13/17] TST: do not use reserved words for type names --- blark/tests/test_transformer.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/blark/tests/test_transformer.py b/blark/tests/test_transformer.py index 33ff4ee..425c63d 100644 --- a/blark/tests/test_transformer.py +++ b/blark/tests/test_transformer.py @@ -394,7 +394,10 @@ def test_bool_literal_roundtrip(name, value, expected): param("var1_init_decl", "stVar1, stVar2 : (Value1 := 1, Value2 := 2) INT := Value1"), param("structure_element_declaration", "Name1, Name2 : INT"), param("structure_type_declaration", "TypeName :\nSTRUCT\nEND_STRUCT"), - param("structure_type_declaration", "TypeName EXTENDS Other.Type :\nSTRUCT\nEND_STRUCT"), + param( + "structure_type_declaration", + "TypeName EXTENDS Other.SomeType :\nSTRUCT\nEND_STRUCT", + ), param("structure_type_declaration", "TypeName : POINTER TO\nSTRUCT\nEND_STRUCT"), param("structure_type_declaration", tf.multiline_code_block( """ @@ -896,7 +899,7 @@ def test_type_name_roundtrip(rule_name, value): iValue := 1; END_IF END_IF - Method(); + MethodName(); RETURN; END_FUNCTION_BLOCK """ @@ -911,7 +914,7 @@ def test_type_name_roundtrip(rule_name, value): iValue := 1; END_IF END_IF - Method(); + MethodName(); ReturnStatus := mReturnStatus; END_FUNCTION_BLOCK """ @@ -926,7 +929,7 @@ def test_type_name_roundtrip(rule_name, value): iValue := 1; END_IF END_IF - Method(); + MethodName(); ContinueWorking := somethingElse; END_FUNCTION_BLOCK """ @@ -941,7 +944,7 @@ def test_type_name_roundtrip(rule_name, value): iValue := 1; END_IF END_IF - Method(); + MethodName(); BreakWork := somethingElse; END_FUNCTION_BLOCK """ @@ -956,7 +959,7 @@ def test_type_name_roundtrip(rule_name, value): iValue := 1; END_IF END_IF - Method(); + MethodName(); ExitWork := somethingElse; END_FUNCTION_BLOCK """ @@ -964,7 +967,7 @@ def test_type_name_roundtrip(rule_name, value): param("function_block_type_declaration", tf.multiline_code_block( """ FUNCTION_BLOCK fbName - Method(); + MethodName(); IF 1 THEN EXIT; END_IF @@ -974,7 +977,7 @@ def test_type_name_roundtrip(rule_name, value): param("function_block_type_declaration", tf.multiline_code_block( """ FUNCTION_BLOCK fbName - Method(); + MethodName(); IF 1 THEN CONTINUE; END_IF From 1a73683fcee6db34440d808273623b8d2d03f0e8 Mon Sep 17 00:00:00 2001 From: Ken Lauer <5139267+klauer@users.noreply.github.com> Date: Thu, 10 Jul 2025 17:59:43 -0700 Subject: [PATCH 14/17] FIX: Interfaces don't have actions --- blark/summary.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/blark/summary.py b/blark/summary.py index 315c753..f947efd 100644 --- a/blark/summary.py +++ b/blark/summary.py @@ -166,7 +166,13 @@ def from_declaration( cls, item: Union[tf.InitDeclaration, tf.StructureElementDeclaration, tf.UnionElementDeclaration], parent: Optional[ - Union[tf.Function, tf.Method, tf.FunctionBlock, tf.Property, tf.StructureTypeDeclaration] + Union[ + tf.Function, + tf.Method, + tf.FunctionBlock, + tf.Property, + tf.StructureTypeDeclaration, + ] ] = None, block_header: str = "unknown", filename: Optional[pathlib.Path] = None, @@ -582,7 +588,6 @@ def from_function_block( return summary - def squash_base_extends( self, function_blocks: Dict[str, FunctionBlockSummary] ) -> FunctionBlockSummary: @@ -650,7 +655,7 @@ def __getitem__( decl = get_case_insensitive(self.declarations, key) if decl is not None: return decl - for item in self.actions + self.methods + self.properties: + for item in self.methods + self.properties: if item.name.lower() == key.lower(): return item raise KeyError(key) From 5c39d666c0dfe3c49b4025318c65a98d6d49e0a0 Mon Sep 17 00:00:00 2001 From: Ken Lauer <5139267+klauer@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:48:16 -0700 Subject: [PATCH 15/17] FIX: round-trip both first/second access specifier in property --- blark/iec.lark | 2 +- blark/tests/test_transformer.py | 20 +++++++++++++++++++- blark/transform.py | 7 +++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/blark/iec.lark b/blark/iec.lark index 2a73004..32070d3 100644 --- a/blark/iec.lark +++ b/blark/iec.lark @@ -611,7 +611,7 @@ function_block_method_declaration: _METHOD [ access_specifier ] DOTTED_IDENTIFIE ?property_return_type: _located_var_spec_init -function_block_property_declaration: _PROPERTY [ access_specifier ] DOTTED_IDENTIFIER [ ":" property_return_type ] ";"* [ access_specifier ] property_var_declaration* [ function_block_body ] _END_PROPERTY ";"* +function_block_property_declaration: _PROPERTY [ access_specifier ] DOTTED_IDENTIFIER [ ":" property_return_type ] [ access_specifier ] ";"* property_var_declaration* [ function_block_body ] _END_PROPERTY ";"* // B.1.5.3 ?program_type_name: IDENTIFIER diff --git a/blark/tests/test_transformer.py b/blark/tests/test_transformer.py index 425c63d..1fa5a50 100644 --- a/blark/tests/test_transformer.py +++ b/blark/tests/test_transformer.py @@ -59,7 +59,7 @@ def roundtrip_rule(rule_name: str, value: str, expected: Optional[str] = None): print("\n\nTransformed:") print(repr(transformed)) - print("\n\nOr:") + print("\n\nAs source code:") print(transformed) if expected is None: expected = value @@ -1063,6 +1063,24 @@ def test_type_name_roundtrip(rule_name, value): END_PROPERTY """ )), + param("function_block_property_declaration", tf.multiline_code_block( + """ + {attribute 'monitoring' := 'call'} + PROPERTY PRIVATE p_fActValue : LREAL PROTECTED + VAR + END_VAR + END_PROPERTY + """ + )), # PR #111 + param("function_block_property_declaration", tf.multiline_code_block( + """ + {attribute 'monitoring' := 'call'} + PROPERTY p_fActValue : LREAL PROTECTED + VAR + END_VAR + END_PROPERTY + """ + )), # PR #111 param("function_block_property_declaration", tf.multiline_code_block( """ PROPERTY PropertyName : RETURNTYPE diff --git a/blark/transform.py b/blark/transform.py index 58b06f8..6329ba7 100644 --- a/blark/transform.py +++ b/blark/transform.py @@ -3264,6 +3264,7 @@ class Property: access: Optional[AccessSpecifier] name: lark.Token return_type: Optional[LocatedVariableSpecInit] + access_override: Optional[AccessSpecifier] declarations: List[VariableDeclarationBlock] body: Optional[FunctionBody] meta: Optional[Meta] = meta_field() @@ -3273,14 +3274,15 @@ def from_lark( access: Optional[AccessSpecifier], name: lark.Token, return_type: Optional[LocatedVariableSpecInit], - access_: Optional[AccessSpecifier], + access_override: Optional[AccessSpecifier], *args ) -> Property: *declarations, body = args return Property( name=name, - access=access or access_, + access=access, return_type=return_type, + access_override=access_override, declarations=list(declarations), body=body, ) @@ -3288,6 +3290,7 @@ def from_lark( def __str__(self) -> str: access_and_name = join_if(self.access, " ", self.name) property = join_if(access_and_name, " : ", self.return_type) + property = join_if(property, " ", self.access_override) return "\n".join( line for line in ( From fd7d0977e60e090e2166ea19122a743b75356a1a Mon Sep 17 00:00:00 2001 From: Ken Lauer <5139267+klauer@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:57:22 -0700 Subject: [PATCH 16/17] TST: extra semicolons after VAR; --- blark/tests/test_transformer.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/blark/tests/test_transformer.py b/blark/tests/test_transformer.py index 1fa5a50..33de637 100644 --- a/blark/tests/test_transformer.py +++ b/blark/tests/test_transformer.py @@ -2,6 +2,7 @@ import os import pathlib +import textwrap from typing import List, Optional import lark @@ -1872,6 +1873,33 @@ def test_labeled_statements_roundtrip(statements: str, labels: List[str]): assert isinstance(transformed, tf.StatementList) +@pytest.mark.parametrize( + ("source", "expected"), + [ + pytest.param( + """\ + FUNCTION_BLOCK fbName + VAR; + END_VAR + END_FUNCTION_BLOCK + """, + """\ + FUNCTION_BLOCK fbName + VAR + END_VAR + END_FUNCTION_BLOCK + """, + id="pr111-var-semicolon" + ) + ] +) +def test_extra_semicolons(source: str, expected: str) -> None: + parsed = parse_source_code(source) + tf_source = parsed.transform() + transformed = tf_source.items[0] + assert str(transformed).strip() == textwrap.dedent(expected).strip() + + @pytest.mark.skipif( os.environ.get("BLARK_CHECK_GRAMMAR_COVERAGE", "") != "1", reason="Optional grammar coverage check not selected" From 5e599ca422416ef9333c9ef55d0e7bbc464f5582 Mon Sep 17 00:00:00 2001 From: Ken Lauer <5139267+klauer@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:32:47 -0700 Subject: [PATCH 17/17] TST: a bit more blark.summary coverage --- blark/tests/test_summary.py | 116 ++++++++++++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 13 deletions(-) diff --git a/blark/tests/test_summary.py b/blark/tests/test_summary.py index 4f0b849..9e9d939 100644 --- a/blark/tests/test_summary.py +++ b/blark/tests/test_summary.py @@ -7,8 +7,9 @@ from .. import transform as tf from ..dependency_store import DependencyStore, PlcProjectMetadata from ..parse import parse_source_code -from ..summary import (CodeSummary, DeclarationSummary, FunctionBlockSummary, - MethodSummary, PropertySummary) +from ..summary import (ActionSummary, CodeSummary, DeclarationSummary, + FunctionBlockSummary, FunctionSummary, MethodSummary, + PropertySummary, text_outline) @dataclass @@ -21,8 +22,7 @@ class DeclarationCheck: def check_declarations( - container: dict[str, DeclarationSummary], - checks: list[DeclarationCheck] + container: dict[str, DeclarationSummary], checks: list[DeclarationCheck] ): """ Check that ``container`` has the listed declarations - and only those. @@ -63,12 +63,13 @@ def check_declarations( def project_a(store: DependencyStore) -> PlcProjectMetadata: # This is project a from the git submodule included with the blark test # suite - proj, = store.get_dependency("project_a") + (proj,) = store.get_dependency("project_a") return proj def test_project_a_summary(project_a: PlcProjectMetadata): summary = project_a.summary + print(text_outline(summary)) assert list(summary.function_blocks) == ["FB_ProjectA"] @@ -96,12 +97,13 @@ def test_project_a_summary(project_a: PlcProjectMetadata): def project_b(store: DependencyStore) -> PlcProjectMetadata: # This is project b from the git submodule included with the blark test # suite - proj, = store.get_dependency("project_b") + (proj,) = store.get_dependency("project_b") return proj def test_project_b_summary(project_b: PlcProjectMetadata): summary = project_b.summary + print(text_outline(summary)) st_projectb = summary.data_types["ST_ProjectB"] assert list(st_projectb.declarations) == ["iProjectB"] @@ -124,12 +126,14 @@ def twincat_general_281(store: DependencyStore) -> PlcProjectMetadata: # included with the blark test suite. # This is *not* the full version from pcdshub/lcls-twincat-general, but # rather a part of it for the purposes of the blark test suite. - proj, = store.get_dependency("LCLS_General", "v2.8.1") + (proj,) = store.get_dependency("LCLS_General", "v2.8.1") return proj def test_twincat_general(twincat_general_281: PlcProjectMetadata): summary = twincat_general_281.summary + print(text_outline(summary)) + assert list(summary.function_blocks) == ["FB_LogHandler"] loghandler = summary.function_blocks["FB_LogHandler"] @@ -151,7 +155,7 @@ def test_twincat_general(twincat_general_281: PlcProjectMetadata): base_type="UINT", value="5", ), - ] + ], ) # Ensure comments transferred over assert loghandler.implementation is None @@ -179,7 +183,7 @@ def test_twincat_general(twincat_general_281: PlcProjectMetadata): value="3", comments=["// Maximum number of ports (4) on ESC"], ), - ] + ], ) defaults = summary.globals["DefaultGlobals"] @@ -196,7 +200,7 @@ def test_twincat_general(twincat_general_281: PlcProjectMetadata): name="fTimeStamp", base_type="LREAL", ), - ] + ], ) system = summary.data_types["ST_System"] @@ -240,7 +244,7 @@ def test_twincat_general(twincat_general_281: PlcProjectMetadata): location="%I*", comments=[ "{attribute 'naming' := 'omit'}", - "(* AMS Net ID used for FB_EcatDiag, among others *)" + "(* AMS Net ID used for FB_EcatDiag, among others *)", ], ), ], @@ -249,6 +253,8 @@ def test_twincat_general(twincat_general_281: PlcProjectMetadata): def test_twincat_general_interface(twincat_general_281: PlcProjectMetadata): summary = twincat_general_281.summary + print(text_outline(summary)) + assert set(str(itf) for itf in summary.interfaces) == {"I_Base", "I_Interface"} base = summary.interfaces["I_Base"] itf = summary.interfaces["I_Interface"] @@ -261,7 +267,7 @@ def test_twincat_general_interface(twincat_general_281: PlcProjectMetadata): base_type="INT", value=None, ), - ] + ], ) check_declarations( @@ -294,7 +300,7 @@ def test_twincat_general_interface(twincat_general_281: PlcProjectMetadata): base_type="INT", value=None, ), - ] + ], ) assert {prop.name for prop in base.properties} == {"BaseProperty"} @@ -320,6 +326,8 @@ def test_twincat_general_interface(twincat_general_281: PlcProjectMetadata): assert isinstance(method1, MethodSummary) assert method1.return_type == "BOOL" + assert list(method1.declarations_by_block) == [] + def test_isolated_summary(): declarations = textwrap.dedent( @@ -354,6 +362,8 @@ def test_isolated_summary(): parsed = parse_source_code(declarations) summary = CodeSummary.from_parse_results(parsed, squash=True) + print("Parsed summary:") + print(str(summary)) fb_test = summary["FB_Test"] assert isinstance(fb_test, FunctionBlockSummary) @@ -361,6 +371,8 @@ def test_isolated_summary(): assert fb_test.declarations["iValue"].base_type == "INT" st_struct = fb_test.declarations["stStruct"] + assert fb_test["stStruct"] is st_struct + assert st_struct.base_type == "StructureType" assert summary.find_code_object_by_dotted_name("FB_Test.stStruct") is st_struct @@ -402,3 +414,81 @@ def test_isolated_summary(): assert runner.base_type == "FB_Runner" assert runner.type == "ARRAY [1..2] OF FB_Runner[(name := 'one'), (name := 'two')]" assert runner.value == "None" + + +def test_function_summary(): + declarations = textwrap.dedent( + """\ + FUNCTION F_Test : INT + VAR_INPUT + iValue : INT; + END_VAR + END_FUNCTION + """ + ) + + parsed = parse_source_code(declarations) + summary = CodeSummary.from_parse_results(parsed, squash=True) + print("Parsed summary:") + print(str(summary)) + + f_test = summary["F_Test"] + assert isinstance(f_test, FunctionSummary) + assert f_test.name == "F_Test" + assert f_test.return_type == "INT" + assert f_test["iValue"].base_type == "INT" + + +def test_action_summary(): + declarations = textwrap.dedent( + """\ + FUNCTION_BLOCK fbName + END_FUNCTION_BLOCK + ACTION ActName : + iValue := iValue + 2; + END_ACTION + """ + ) + + parsed = parse_source_code(declarations) + summary = CodeSummary.from_parse_results(parsed, squash=True) + print("Parsed summary:") + print(str(summary)) + + fb = summary["fbName"] + (act,) = fb.actions + assert isinstance(act, ActionSummary) + assert act.name == "ActName" + assert str(act.item.body) == "iValue := iValue + 2;" + + +def test_fb_extends_summary(): + declarations = textwrap.dedent( + """\ + FUNCTION_BLOCK fbBase + VAR_INPUT + iBase : INT := 1; + END_VAR + END_FUNCTION_BLOCK + FUNCTION_BLOCK fbExtended EXTENDS fbBase + VAR_INPUT + iExtended : INT := 2; + END_VAR + END_FUNCTION_BLOCK + """ + ) + + parsed = parse_source_code(declarations) + summary = CodeSummary.from_parse_results(parsed, squash=True) + print("Parsed summary:") + print(str(summary)) + + base = summary["fbBase"] + assert base.declarations["iBase"].base_type == "INT" + assert base.declarations["iBase"].value == "1" + + extended = summary["fbExtended"] + assert extended.declarations["iBase"].base_type == "INT" + assert extended.declarations["iBase"].value == "1" + assert extended.declarations["iExtended"].base_type == "INT" + assert extended.declarations["iExtended"].value == "2"