Skip to content

Commit bc5a458

Browse files
authored
Merge pull request #73 from klauer/enh_interface
ENH: interface support
2 parents 2bd8165 + 024550e commit bc5a458

File tree

8 files changed

+386
-34
lines changed

8 files changed

+386
-34
lines changed

blark/iec.lark

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ _library_element_declaration: data_type_declaration
2626
| function_block_type_declaration
2727
| function_block_method_declaration
2828
| function_block_property_declaration
29+
| interface_declaration
2930
| program_declaration
3031
| global_var_declarations
3132
| action
@@ -637,6 +638,17 @@ program_access_decls: "VAR_ACCESS"i (program_access_decl ";"+)+ "END_VAR"i ";"*
637638

638639
program_access_decl: access_name ":" symbolic_variable ":" non_generic_type_name [ access_direction ]
639640

641+
// Beckhoff/codesys-specific INTERFACE definition describing variables, methods,
642+
// and properties of other POUs
643+
644+
?interface_var_declaration: input_declarations
645+
| output_declarations
646+
| input_output_declarations
647+
| external_var_declarations
648+
| var_declarations
649+
650+
interface_declaration: "INTERFACE"i IDENTIFIER [ extends ] interface_var_declaration* "END_INTERFACE"i ";"*
651+
640652
// B.1.6
641653
?step_name: IDENTIFIER
642654
INITIAL_STEP: "INITIAL_STEP"i

blark/parse.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class BlarkStartingRule(enum.Enum):
4848
function_block_type_declaration = enum.auto()
4949
function_declaration = enum.auto()
5050
global_var_declarations = enum.auto()
51+
interface_declaration = enum.auto()
5152
program_declaration = enum.auto()
5253
statement_list = enum.auto()
5354

blark/solution.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,11 @@ def filename_from_xml(xml: Optional[lxml.etree.Element]) -> Optional[pathlib.Pat
179179

180180
if root.docinfo.URL is None:
181181
return None
182-
return pathlib.Path(root.docinfo.URL)
182+
url = str(root.docinfo.URL)
183+
if url.startswith("file:/"):
184+
# Note: this only seems to be necessary on Windows and I'm not entirely sure why
185+
url = url[len("file:/"):]
186+
return pathlib.Path(url)
183187

184188

185189
def get_child_text(
@@ -577,13 +581,37 @@ def __post_init__(self, xml: Optional[lxml.etree.Element]) -> None:
577581
]
578582

579583
def to_blark(self) -> list[Union[BlarkCompositeSourceItem, BlarkSourceItem]]:
580-
# TODO: blark does not yet understand INTERFACE
581-
# if self.decl is not None:
582-
# res = self.decl.to_blark()
583-
# for item in res:
584-
# item.user = self
585-
# return res
586-
return []
584+
if self.source_type is None:
585+
raise RuntimeError("No source type set?")
586+
587+
parts = []
588+
589+
if self.decl is not None:
590+
identifier = self.decl.identifier
591+
parts.extend(self.decl.to_blark())
592+
else:
593+
identifier = None
594+
595+
for part in self.parts:
596+
if isinstance(part, ContainsBlarkCode):
597+
for item in part.to_blark():
598+
if identifier:
599+
item.identifier = f"{identifier}.{item.identifier}"
600+
item.user = self
601+
parts.append(item)
602+
elif not isinstance(part, (TcExtraInfo, TcUnknownXml)):
603+
raise NotImplementedError(
604+
f"TcPOU portion {type(part)} not yet implemented"
605+
)
606+
607+
return [
608+
BlarkCompositeSourceItem(
609+
filename=self.filename,
610+
identifier=self.decl.identifier if self.decl is not None else "unknown",
611+
parts=parts,
612+
user=self,
613+
)
614+
]
587615

588616
def _serialize(self, primary: lxml.etree.Element) -> None:
589617
super()._serialize(primary)

blark/summary.py

Lines changed: 163 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,23 @@ def from_declaration(
209209
filename=filename,
210210
**Summary.get_meta_kwargs(item.meta),
211211
)
212+
elif isinstance(item, tf.ExternalVariableDeclaration):
213+
location = str(getattr(item.spec, "location", None))
214+
init = str(getattr(item.spec, "init", None))
215+
type_ = getattr(init, "full_type_name", str(item.spec))
216+
base_type = getattr(init, "base_type_name", str(item.spec))
217+
result[item.name] = DeclarationSummary(
218+
name=str(item.name),
219+
item=item,
220+
location=location,
221+
block=block_header,
222+
type=type_,
223+
base_type=base_type,
224+
value="None", # TODO
225+
parent=parent.name if parent is not None else "",
226+
filename=filename,
227+
**Summary.get_meta_kwargs(item.meta),
228+
)
212229
else:
213230
raise NotImplementedError(f"TODO: {type(item)}")
214231

@@ -479,7 +496,9 @@ class FunctionBlockSummary(Summary):
479496
methods: List[MethodSummary] = field(default_factory=list)
480497
properties: List[PropertySummary] = field(default_factory=list)
481498

482-
def __getitem__(self, key: str) -> DeclarationSummary:
499+
def __getitem__(
500+
self, key: str
501+
) -> Union[DeclarationSummary, MethodSummary, PropertySummary, ActionSummary]:
483502
if key in self.declarations:
484503
return self.declarations[key]
485504
for item in self.actions + self.methods + self.properties:
@@ -528,7 +547,7 @@ def squash_base_extends(
528547
if self.extends is None:
529548
return self
530549

531-
extends_from = function_blocks.get(self.extends, None)
550+
extends_from = function_blocks.get(str(self.extends), None)
532551
if extends_from is None:
533552
return self
534553

@@ -539,6 +558,7 @@ def squash_base_extends(
539558
declarations.update(self.declarations)
540559
actions = list(extends_from.actions) + self.actions
541560
methods = list(extends_from.methods) + self.methods
561+
properties = list(extends_from.properties) + self.properties
542562
return FunctionBlockSummary(
543563
name=self.name,
544564
comments=extends_from.comments + self.comments,
@@ -551,6 +571,101 @@ def squash_base_extends(
551571
declarations=declarations,
552572
actions=actions,
553573
methods=methods,
574+
properties=properties,
575+
squashed=True,
576+
)
577+
578+
579+
@dataclass
580+
class InterfaceSummary(Summary):
581+
"""Summary representation of an Interfae."""
582+
583+
name: str
584+
source_code: str
585+
item: tf.Interface
586+
extends: Optional[str]
587+
squashed: bool
588+
declarations: Dict[str, DeclarationSummary] = field(default_factory=dict)
589+
methods: List[MethodSummary] = field(default_factory=list)
590+
properties: List[PropertySummary] = field(default_factory=list)
591+
# TwinCAT IDE doesn't allow for actions to be added to interfaces, it
592+
# seems. Overlap with methods?
593+
# actions: List[ActionSummary] = field(default_factory=list)
594+
595+
def __getitem__(
596+
self, key: str
597+
) -> Union[DeclarationSummary, MethodSummary, PropertySummary]:
598+
if key in self.declarations:
599+
return self.declarations[key]
600+
for item in self.methods + self.properties:
601+
if item.name == key:
602+
return item
603+
raise KeyError(key)
604+
605+
@property
606+
def declarations_by_block(self) -> Dict[str, Dict[str, DeclarationSummary]]:
607+
result = {}
608+
for decl in self.declarations.values():
609+
result.setdefault(decl.block, {})[decl.name] = decl
610+
return result
611+
612+
@classmethod
613+
def from_interface(
614+
cls,
615+
itf: tf.Interface,
616+
source_code: Optional[str] = None,
617+
filename: Optional[pathlib.Path] = None,
618+
) -> InterfaceSummary:
619+
if source_code is None:
620+
source_code = str(itf)
621+
622+
summary = InterfaceSummary(
623+
name=itf.name,
624+
item=itf,
625+
source_code=source_code,
626+
filename=filename,
627+
extends=itf.extends.name if itf.extends else None,
628+
squashed=False,
629+
**Summary.get_meta_kwargs(itf.meta),
630+
)
631+
632+
for decl in itf.declarations:
633+
summary.declarations.update(
634+
DeclarationSummary.from_block(decl, parent=itf, filename=filename)
635+
)
636+
637+
return summary
638+
639+
def squash_base_extends(
640+
self, interfaces: Dict[str, InterfaceSummary]
641+
) -> InterfaceSummary:
642+
"""Squash the "EXTENDS" INTERFACE into this one."""
643+
if self.extends is None:
644+
return self
645+
646+
extends_from = interfaces.get(str(self.extends), None)
647+
if extends_from is None:
648+
return self
649+
650+
if extends_from.extends:
651+
extends_from = extends_from.squash_base_extends(interfaces)
652+
653+
declarations = dict(extends_from.declarations)
654+
declarations.update(self.declarations)
655+
methods = list(extends_from.methods) + self.methods
656+
properties = list(extends_from.properties) + self.properties
657+
return InterfaceSummary(
658+
name=self.name,
659+
comments=extends_from.comments + self.comments,
660+
pragmas=extends_from.pragmas + self.pragmas,
661+
meta=self.meta,
662+
filename=self.filename,
663+
source_code="\n\n".join((extends_from.source_code, self.source_code)),
664+
item=self.item,
665+
extends=self.extends,
666+
declarations=declarations,
667+
properties=properties,
668+
methods=methods,
554669
squashed=True,
555670
)
556671

@@ -622,7 +737,7 @@ def squash_base_extends(
622737
if self.extends is None:
623738
return self
624739

625-
extends_from = data_types.get(self.extends, None)
740+
extends_from = data_types.get(str(self.extends), None)
626741
if extends_from is None:
627742
return self
628743

@@ -631,7 +746,6 @@ def squash_base_extends(
631746

632747
declarations = dict(extends_from.declarations)
633748
declarations.update(self.declarations)
634-
raise
635749
return DataTypeSummary(
636750
name=self.name,
637751
type=self.type,
@@ -767,15 +881,16 @@ class CodeSummary:
767881
data_types: Dict[str, DataTypeSummary] = field(default_factory=dict)
768882
programs: Dict[str, ProgramSummary] = field(default_factory=dict)
769883
globals: Dict[str, GlobalVariableSummary] = field(default_factory=dict)
770-
# interfaces: Dict[str, ...] = field(default_factory=dict) # TODO
884+
interfaces: Dict[str, InterfaceSummary] = field(default_factory=dict)
771885

772886
def __str__(self):
773887
attr_to_header = {
888+
"data_types": "Data Types",
889+
"globals": "Global Variable Declarations",
890+
"interfaces": "Interface Declarations",
774891
"functions": "Functions",
775892
"function_blocks": "Function Blocks",
776-
"data_types": "Data Types",
777893
"programs": "Programs",
778-
"globals": "Global Variable Declarations",
779894
}
780895
summary_text = []
781896
for attr, header in attr_to_header.items():
@@ -892,6 +1007,7 @@ def append(self, other: CodeSummary, namespace: Optional[str] = None):
8921007
self.data_types.update(other.data_types)
8931008
self.globals.update(other.globals)
8941009
self.programs.update(other.programs)
1010+
self.interfaces.update(other.interfaces)
8951011

8961012
if namespace:
8971013
# LCLS_General.GVL_Logger and GVL_Logger are equally valid
@@ -906,8 +1022,13 @@ def append(self, other: CodeSummary, namespace: Optional[str] = None):
9061022
# for name, item in other.programs.items():
9071023
# self.programs[f"{namespace}.{name}"] = item
9081024

1025+
self.squash()
1026+
9091027
@staticmethod
910-
def from_parse_results(all_parsed_items: list[ParseResult]) -> CodeSummary:
1028+
def from_parse_results(
1029+
all_parsed_items: list[ParseResult],
1030+
squash: bool = True,
1031+
) -> CodeSummary:
9111032
result = CodeSummary()
9121033

9131034
def get_code_by_meta(parsed: ParseResult, meta: Optional[tf.Meta]) -> str:
@@ -958,16 +1079,19 @@ def new_context(summary: Summary):
9581079
def push_context(summary: Summary):
9591080
context.append(summary)
9601081

961-
def get_pou_context() -> Union[ProgramSummary, FunctionBlockSummary]:
1082+
def get_pou_context() -> Union[
1083+
ProgramSummary, FunctionBlockSummary, InterfaceSummary
1084+
]:
9621085
for item in reversed(context):
963-
if isinstance(item, (ProgramSummary, FunctionBlockSummary)):
1086+
if isinstance(
1087+
item, (ProgramSummary, FunctionBlockSummary, InterfaceSummary)
1088+
):
9641089
return item
9651090

9661091
raise ValueError(
9671092
"Expected to parse a POU prior to this but none were in the context "
9681093
"list. Code summaries of PROPERTY objects, for example, require "
969-
"that a FUNCTION_BLOCK (or other PROPERTY-containing POU) be "
970-
"parsed previously."
1094+
"that a FUNCTION_BLOCK, INTERFACE, or PROGRAM be parsed previously."
9711095
)
9721096

9731097
for parsed in all_parsed_items:
@@ -1047,6 +1171,14 @@ def get_pou_context() -> Union[ProgramSummary, FunctionBlockSummary]:
10471171
)
10481172
result.programs[item.name] = summary
10491173
new_context(summary)
1174+
elif isinstance(item, tf.Interface):
1175+
summary = InterfaceSummary.from_interface(
1176+
item,
1177+
source_code=get_code_by_meta(parsed, item.meta),
1178+
filename=parsed.filename,
1179+
)
1180+
result.interfaces[item.name] = summary
1181+
new_context(summary)
10501182
elif isinstance(item, tf.StatementList):
10511183
if parsed.item.type != SourceType.action:
10521184
add_implementation(parsed, item)
@@ -1064,21 +1196,33 @@ def get_pou_context() -> Union[ProgramSummary, FunctionBlockSummary]:
10641196
)
10651197
parent.actions.append(action)
10661198
else:
1199+
raise ValueError(type(item))
10671200
logger.warning("Unhandled: %s", type(item))
10681201

1069-
for name, item in list(result.function_blocks.items()):
1202+
if squash:
1203+
result.squash()
1204+
1205+
return result
1206+
1207+
def squash(self) -> None:
1208+
"""Squash derived interfaces/etc to include base summaries."""
1209+
for name, item in list(self.function_blocks.items()):
10701210
if item.extends and not item.squashed:
1071-
result.function_blocks[name] = item.squash_base_extends(
1072-
result.function_blocks
1211+
self.function_blocks[name] = item.squash_base_extends(
1212+
self.function_blocks
10731213
)
10741214

1075-
for name, item in list(result.data_types.items()):
1215+
for name, item in list(self.data_types.items()):
10761216
if item.extends and not item.squashed:
1077-
result.data_types[name] = item.squash_base_extends(
1078-
result.data_types
1217+
self.data_types[name] = item.squash_base_extends(
1218+
self.data_types
10791219
)
10801220

1081-
return result
1221+
for name, item in list(self.interfaces.items()):
1222+
if item.extends and not item.squashed:
1223+
self.interfaces[name] = item.squash_base_extends(
1224+
self.interfaces
1225+
)
10821226

10831227

10841228
@dataclass

0 commit comments

Comments
 (0)