diff --git a/.gitignore b/.gitignore index 9bf885e..081c539 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,8 @@ /dist *.egg-info .coverage +/docs/build +.DS_Store +# These are auto-generated: +/docs/source/api.rst +/docs/source/api diff --git a/blark/iec.lark b/blark/iec.lark index 1bd003e..5c6f217 100644 --- a/blark/iec.lark +++ b/blark/iec.lark @@ -739,13 +739,16 @@ _statement: ";" // B.3.2.1 no_op_statement: _variable ";"+ -assignment_statement: _variable ":=" ( _variable ":=" )* expression ";"+ +assignment_statement: _variable ASSIGNMENT ( _variable ASSIGNMENT )* expression ";"+ -set_statement: _variable "S="i expression ";"+ +SET_ASSIGNMENT: "S="i +set_statement: _variable SET_ASSIGNMENT expression ";"+ -reset_statement: _variable "R="i expression ";"+ +RESET_ASSIGNMENT: "R="i +reset_statement: _variable RESET_ASSIGNMENT expression ";"+ -reference_assignment_statement: _variable "REF="i expression ";"+ +REF_ASSIGNMENT: "REF="i +reference_assignment_statement: _variable REF_ASSIGNMENT expression ";"+ // B.3.2.2 return_statement.1: "RETURN"i ";"+ diff --git a/blark/parse.py b/blark/parse.py index 5470eaf..b2132f0 100644 --- a/blark/parse.py +++ b/blark/parse.py @@ -10,14 +10,13 @@ import logging import pathlib import sys +import typing from dataclasses import dataclass from typing import Generator, Optional, Sequence, Type, TypeVar, Union import lark -import blark - -from . import solution, summary +from . import solution from . import transform as tf from . import util from .input import BlarkCompositeSourceItem, BlarkSourceItem, load_file_by_name @@ -29,6 +28,9 @@ except ImportError: apischema = None +if typing.TYPE_CHECKING: + from .summary import CodeSummary + logger = logging.getLogger(__name__) @@ -65,9 +67,10 @@ def new_parser(start: Optional[list[str]] = None, **kwargs) -> lark.Lark: if start is None: start = [rule.name for rule in BlarkStartingRule] + from . import GRAMMAR_FILENAME return lark.Lark.open_from_package( "blark", - blark.GRAMMAR_FILENAME.name, + GRAMMAR_FILENAME.name, parser="earley", maybe_placeholders=True, propagate_positions=True, @@ -371,9 +374,13 @@ def build_arg_parser(argparser=None): return argparser -def summarize(parsed: list[ParseResult]) -> summary.CodeSummary: +def summarize( + parsed: Union[ParseResult, list[ParseResult]], + squash: bool = True, +) -> CodeSummary: """Get a code summary instance from one or more ParseResult instances.""" - return summary.CodeSummary.from_parse_results(parsed) + from .summary import CodeSummary + return CodeSummary.from_parse_results(parsed, squash=squash) T = TypeVar("T") @@ -525,7 +532,8 @@ def get_items(): if output_summary: summarized = summarize(all_results) if use_json: - print(dump_json(summary.CodeSummary, summarized, include_meta=include_meta)) + from .summary import CodeSummary + print(dump_json(CodeSummary, summarized, include_meta=include_meta)) else: print(summarized) else: diff --git a/blark/summary.py b/blark/summary.py index 6c7dfc4..2b15285 100644 --- a/blark/summary.py +++ b/blark/summary.py @@ -5,21 +5,17 @@ import pathlib import textwrap from dataclasses import dataclass, field, fields, is_dataclass -from typing import (TYPE_CHECKING, Any, Dict, Generator, Iterable, List, - Optional, Tuple, Union) +from typing import Any, Dict, Generator, Iterable, List, Optional, Tuple, Union import lark from . import transform as tf +from .parse import ParseResult from .typing import Literal from .util import Identifier, SourceType LocationType = Literal["input", "output", "memory"] -if TYPE_CHECKING: - from .parse import ParseResult - - logger = logging.getLogger(__name__) @@ -97,6 +93,9 @@ class Summary: filename: Optional[pathlib.Path] meta: Optional[tf.Meta] = field(repr=False) + def __getitem__(self, _: str) -> None: + return None + def __str__(self) -> str: return text_outline(self) or "" @@ -121,7 +120,15 @@ def get_meta_kwargs(meta: Optional[tf.Meta]) -> Dict[str, Any]: class DeclarationSummary(Summary): """Summary representation of a single declaration.""" name: str - item: Union[tf.Declaration, tf.GlobalVariableDeclaration, tf.VariableInitDeclaration] + item: Union[ + tf.Declaration, + tf.GlobalVariableDeclaration, + tf.VariableInitDeclaration, + tf.StructureElementDeclaration, + tf.UnionElementDeclaration, + tf.ExternalVariableDeclaration, + tf.InitDeclaration, + ] parent: Optional[str] location: Optional[str] block: str @@ -129,6 +136,9 @@ class DeclarationSummary(Summary): type: str value: Optional[str] + def __getitem__(self, key: str) -> None: + raise KeyError(f"{self.name}[{key!r}]: declarations do not contain keys") + @property def qualified_name(self) -> str: """Qualified name including parent. For example, ``fbName.DeclName``.""" @@ -154,7 +164,7 @@ def location_type(self) -> Optional[LocationType]: @classmethod def from_declaration( cls, - item: Union[tf.InitDeclaration, tf.StructureElementDeclaration], + item: Union[tf.InitDeclaration, tf.StructureElementDeclaration, tf.UnionElementDeclaration], parent: Optional[ Union[tf.Function, tf.Method, tf.FunctionBlock, tf.StructureTypeDeclaration] ] = None, @@ -162,15 +172,29 @@ def from_declaration( filename: Optional[pathlib.Path] = None, ) -> Dict[str, DeclarationSummary]: result = {} + if isinstance(item, tf.StructureElementDeclaration): result[item.name] = DeclarationSummary( name=str(item.name), item=item, location=item.location.name if item.location else None, block=block_header, - type=item.init.full_type_name, - base_type=item.init.base_type_name, - value=str(item.init.value), + type=item.full_type_name, # TODO -> get_type_summary? + base_type=item.base_type_name, + value=str(item.value), + parent=parent.name if parent is not None else "", + filename=filename, + **Summary.get_meta_kwargs(item.meta), + ) + elif isinstance(item, tf.UnionElementDeclaration): + result[item.name] = DeclarationSummary( + name=str(item.name), + item=item, + location=None, + block=block_header, + type=item.spec.full_type_name, + base_type=item.spec.base_type_name, + value="", parent=parent.name if parent is not None else "", filename=filename, **Summary.get_meta_kwargs(item.meta), @@ -268,8 +292,12 @@ def from_block( result = {} for decl in block.items: result.update( - cls.from_declaration(decl, parent=parent, block_header=block.block_header, - filename=filename) + cls.from_declaration( + decl, + parent=parent, + block_header=block.block_header, + filename=filename, + ) ) return result @@ -282,7 +310,7 @@ class ActionSummary(Summary): source_code: str implementation: Optional[tf.StatementList] = None - def __getitem__(self, key: str): + def __getitem__(self, key: str) -> None: raise KeyError(f"{key}: Actions do not contain declarations") @classmethod @@ -728,6 +756,17 @@ def from_data_type( ) ) + if isinstance(dtype, tf.UnionTypeDeclaration): + for decl in dtype.declarations: + summary.declarations.update( + DeclarationSummary.from_declaration( + decl, + parent=dtype, + block_header="UNION", + filename=filename, + ) + ) + return summary def squash_base_extends( @@ -871,6 +910,29 @@ def path_to_file_and_line(path: List[Summary]) -> List[Tuple[pathlib.Path, int]] return [(part.filename, part.item.meta.line) for part in path] +TopLevelCodeSummaryType = Union[ + FunctionSummary, + FunctionBlockSummary, + DataTypeSummary, + ProgramSummary, + InterfaceSummary, + GlobalVariableSummary, +] + +NestedCodeSummaryType = Union[ + DeclarationSummary, + MethodSummary, + PropertySummary, + ActionSummary, + PropertyGetSetSummary, +] + +CodeSummaryType = Union[ + TopLevelCodeSummaryType, + NestedCodeSummaryType, +] + + @dataclass class CodeSummary: """Summary representation of a set of code - functions, function blocks, etc.""" @@ -883,7 +945,7 @@ class CodeSummary: globals: Dict[str, GlobalVariableSummary] = field(default_factory=dict) interfaces: Dict[str, InterfaceSummary] = field(default_factory=dict) - def __str__(self): + def __str__(self) -> str: attr_to_header = { "data_types": "Data Types", "globals": "Global Variable Declarations", @@ -915,17 +977,43 @@ def __str__(self): return "\n".join(summary_text) - def find(self, name: str) -> Optional[Summary]: + def find(self, name: str) -> Optional[CodeSummaryType]: """Find a declaration or other item by its qualified name.""" - path = self.find_path(name) + path = self.find_path(name, allow_partial=False) return path[-1] if path else None - def find_path(self, name: str) -> Optional[List[Summary]]: - """Given a qualified variable name, find its Declaration.""" + def find_path( + self, + name: str, + allow_partial: bool = False, + ) -> Optional[List[CodeSummaryType]]: + """ + Given a qualified variable name, find the path of CodeSummary objects top-down. + + For example, a variable declared in a function block would return a + list containing the FunctionBlockSummary and then the + DeclarationSummary. + + Parameters + ---------- + name : str + The qualified ("dotted") variable name to find. + + allow_partial : bool, optional + If an attribute is missing along the way, return the partial + path that was found. + + Returns + ------- + list of CodeSummaryType or None + The full path to the given object. + """ parts = collections.deque(name.split(".")) if len(parts) <= 1: item = self.get_item_by_name(name) - return [item] if item is not None else None + if item is None: + return None + return [item] variable_name = parts.pop() parent = None @@ -938,12 +1026,15 @@ def find_path(self, name: str) -> Optional[List[Summary]]: try: if parent is None: parent = self.get_item_by_name(part) + path.append(parent) else: part_obj = parent[part] path.append(part_obj) part_type = str(part_obj.base_type) parent = self.get_item_by_name(part_type) except KeyError: + if allow_partial: + return path return if parent is None: @@ -952,28 +1043,40 @@ def find_path(self, name: str) -> Optional[List[Summary]]: try: path.append(parent[variable_name]) except KeyError: - # Is it better to give a partial path or no path at all? - ... + if not allow_partial: + return None return path - def find_code_object_by_dotted_name(self, name: str) -> Optional[Summary]: - """Given a qualified code object name, find its Summary object(s).""" - # TODO: These functions are a bit of a mess and confuse even me - # The intent with *this* flavor of 'find' is to take in things like: - # FB_Block.ActionName - # FB_Block.PropertyName.get - # FB_Block.PropertyName.set + def find_code_object_by_dotted_name(self, name: str) -> Optional[CodeSummaryType]: + """ + Given a qualified code object name, find its Summary object(s). + + This works to find CodeSummary objects such as:: + + FB_Block.ActionName + FB_Block.PropertyName.get + FB_Block.PropertyName.set + """ parts = Identifier.from_string(name).parts obj = self.get_item_by_name(parts[0]) if len(parts) == 1: return obj + if obj is None: + raise ValueError(f"No object by the name of {parts[0]} exists") for remainder in parts[1:]: - obj = obj[remainder] + next_obj = obj[remainder] + if next_obj is None: + raise ValueError(f"{name}: {obj} has no attribute {remainder!r}") + obj = next_obj + return obj - def get_all_items_by_name(self, name: str) -> Generator: + def get_all_items_by_name( + self, + name: str, + ) -> Generator[TopLevelCodeSummaryType, None, None]: """Get any code item (function, data type, global variable, etc.) by name.""" for dct in ( self.globals, @@ -988,13 +1091,24 @@ def get_all_items_by_name(self, name: str) -> Generator: except KeyError: ... - def get_item_by_name(self, name: str) -> Optional[Any]: - """Get any code item (function, data type, global variable, etc.) by name.""" + def get_item_by_name(self, name: str) -> Optional[TopLevelCodeSummaryType]: + """ + Get a single code item (function, data type, global variable, etc.) by name. + + Does not handle scenarios where names are shadowed by other + declarations. The first one found will take precedence. + """ try: return next(self.get_all_items_by_name(name)) except StopIteration: return None + def __getitem__(self, name: str) -> TopLevelCodeSummaryType: + item = self.get_item_by_name(name) + if item is None: + raise KeyError(f"{name!r} is not a top-level code object name") + return item + def append(self, other: CodeSummary, namespace: Optional[str] = None): """ In-place add code summary information from another instance. @@ -1026,9 +1140,12 @@ def append(self, other: CodeSummary, namespace: Optional[str] = None): @staticmethod def from_parse_results( - all_parsed_items: list[ParseResult], + all_parsed_items: Union[ParseResult, list[ParseResult]], squash: bool = True, ) -> CodeSummary: + if isinstance(all_parsed_items, ParseResult): + all_parsed_items = [all_parsed_items] + result = CodeSummary() def get_code_by_meta(parsed: ParseResult, meta: Optional[tf.Meta]) -> str: @@ -1115,7 +1232,10 @@ def get_pou_context() -> Union[ result.functions[item.name] = summary new_context(summary) elif isinstance(item, tf.DataTypeDeclaration): - if isinstance(item.declaration, tf.StructureTypeDeclaration): + if isinstance( + item.declaration, + (tf.StructureTypeDeclaration, tf.UnionTypeDeclaration) + ): summary = DataTypeSummary.from_data_type( item.declaration, source_code=get_code_by_meta(parsed, item.declaration.meta), diff --git a/blark/tests/test_summary.py b/blark/tests/test_summary.py index 158b926..46ddb47 100644 --- a/blark/tests/test_summary.py +++ b/blark/tests/test_summary.py @@ -1,3 +1,4 @@ +import textwrap from dataclasses import dataclass from typing import Optional @@ -5,7 +6,9 @@ from .. import transform as tf from ..dependency_store import DependencyStore, PlcProjectMetadata -from ..summary import DeclarationSummary, MethodSummary, PropertySummary +from ..parse import parse_source_code +from ..summary import (CodeSummary, DeclarationSummary, FunctionBlockSummary, + MethodSummary, PropertySummary) @dataclass @@ -316,3 +319,86 @@ def test_twincat_general_interface(twincat_general_281: PlcProjectMetadata): method1 = itf["Method1"] assert isinstance(method1, MethodSummary) assert method1.return_type == "BOOL" + + +def test_isolated_summary(): + declarations = textwrap.dedent( + """\ + TYPE UnionType : + UNION + iValue : DINT; + bValue : BOOL; + END_UNION + END_TYPE + TYPE BaseStructureType : + STRUCT + iBase : DINT; + END_STRUCT + END_TYPE + TYPE StructureType EXTENDS BaseStructureType : + STRUCT + bValue : BOOL; + END_STRUCT + END_TYPE + FUNCTION_BLOCK FB_Test + VAR_INPUT + iValue : INT; + stStruct : StructureType; + U_Union : UnionType; + stFbDecl : FB_Sample(nInitParam := 1) := (nInput := 2, nMyProperty := 3); + astRunner : ARRAY[1..2] OF FB_Runner[(name := 'one'), (name := 'two')]; + END_VAR + END_FUNCTION_BLOCK + """ + ) + + parsed = parse_source_code(declarations) + summary = CodeSummary.from_parse_results(parsed, squash=True) + + fb_test = summary["FB_Test"] + assert isinstance(fb_test, FunctionBlockSummary) + assert fb_test.name == "FB_Test" + assert fb_test.declarations["iValue"].base_type == "INT" + + st_struct = fb_test.declarations["stStruct"] + assert st_struct.base_type == "StructureType" + assert summary.find_code_object_by_dotted_name("FB_Test.stStruct") is st_struct + + ibase = summary.find_path("FB_Test.stStruct.iBase") + assert ibase is not None + assert ibase[0] is fb_test + assert ibase[1] is st_struct + assert isinstance(ibase[2], DeclarationSummary) + assert ibase[2].name == "iBase" + assert ibase[2].base_type == "DINT" + + assert summary.find("FB_Test.stStruct.iBase") is ibase[2] + + bvalue = summary.find_path("FB_Test.stStruct.bValue") + assert bvalue is not None + assert bvalue[0] is fb_test + assert bvalue[1] is st_struct + assert isinstance(bvalue[2], DeclarationSummary) + assert bvalue[2].name == "bValue" + assert bvalue[2].base_type == "BOOL" + + assert summary.find("FB_Test.stStruct.bValue") is bvalue[2] + + union_bvalue = summary.find("FB_Test.U_Union.bValue") + assert union_bvalue is not None + + fbdecl = summary.find("FB_Test.stFbDecl") + assert fbdecl is not None + assert isinstance(fbdecl, DeclarationSummary) + # TODO: not quite right + assert fbdecl.base_type == "FB_Sample" + assert fbdecl.type == "FB_Sample" + assert fbdecl.value == "FB_Sample(nInitParam := 1)" + + runner = summary.find("FB_Test.astRunner") + assert runner is not None + assert isinstance(runner, DeclarationSummary) + # TODO: not quite right + assert runner.base_type == "FB_Runner" + assert runner.type == "ARRAY [1..2] OF FB_Runner[(name := 'one'), (name := 'two')]" + assert runner.value == "None" diff --git a/blark/tests/test_util.py b/blark/tests/test_util.py index f873f07..a72366a 100644 --- a/blark/tests/test_util.py +++ b/blark/tests/test_util.py @@ -5,6 +5,7 @@ import pytest +from .. import util from ..input import BlarkCompositeSourceItem, BlarkSourceItem, BlarkSourceLine from ..util import SourceType, find_and_clean_comments @@ -263,3 +264,56 @@ def test_line_map_composite(): 9: 74, 10: 74, } + + +@pytest.mark.parametrize( + "code, expected", + [ + pytest.param("(abc)", "(abc)"), + pytest.param("()", "()"), + pytest.param("(())", "()"), + pytest.param("()(abc)", "()(abc)"), + pytest.param("((abc))", "(abc)"), + pytest.param("((a)b(c))", "((a)b(c))"), + pytest.param("((((a)b(c))))", "((a)b(c))"), + ], +) +def test_simplify_brackets(code: str, expected: str): + assert util.simplify_brackets(code, "()") == expected + + +@pytest.mark.parametrize( + "code", + [ + pytest.param("()abc))"), + pytest.param("(abc))"), + pytest.param("((a)b(c)))"), + ], +) +def test_simplify_brackets_unbalanced(code: str): + with pytest.raises(ValueError): + util.simplify_brackets(code, "()") + + +@pytest.mark.parametrize( + "code, expected", + [ + pytest.param("(abc)", "(abc)"), + pytest.param("()", "()"), + pytest.param("()(abc)", "()(abc)"), + pytest.param("((a)b(c))", "((a)b(c))"), + pytest.param("(((a)b(c)))", "(((a)b(c)))"), + ], +) +def test_maybe_add_brackets(code: str, expected: str): + assert util.maybe_add_brackets(code, "()") == expected + + +def test_get_grammar_for_class(): + from ..transform import OctalInteger, SourceCode, get_grammar_for_class + assert get_grammar_for_class(SourceCode) == { + "iec_source": "iec_source: _library_element_declaration*", + } + assert get_grammar_for_class(OctalInteger) == { + "octal_integer": '| "8#" OCTAL_STRING -> octal_integer', + } diff --git a/blark/transform.py b/blark/transform.py index 9831cdd..d3c160e 100644 --- a/blark/transform.py +++ b/blark/transform.py @@ -11,8 +11,14 @@ from typing import (Any, Callable, ClassVar, Dict, Generator, List, Optional, Tuple, Type, TypeVar, Union) +try: + from typing import Self +except ImportError: + from typing_extensions import Self + import lark +from . import util from .util import AnyPath, maybe_add_brackets, rebuild_lark_tree_with_line_map T = TypeVar("T") @@ -140,21 +146,35 @@ def wrapper(cls: Type[T]) -> Type[T]: @dataclasses.dataclass class Meta: + """Lark-derived meta information in the form of a dataclass.""" + #: If the metadata information is not yet filled. empty: bool = True + #: Column number. column: Optional[int] = None + #: Comments relating to the line. comments: List[lark.Token] = dataclasses.field(default_factory=list) + #: Containing start column. container_column: Optional[int] = None + #: Containing end column. container_end_column: Optional[int] = None + #: Containing end line. container_end_line: Optional[int] = None + #: Containing start line. container_line: Optional[int] = None + #: Final column number. end_column: Optional[int] = None + #: Final line number. end_line: Optional[int] = None + #: Final character position. end_pos: Optional[int] = None + #: Line number. line: Optional[int] = None + #: Starting character position. start_pos: Optional[int] = None @staticmethod def from_lark(lark_meta: lark.tree.Meta) -> Meta: + """Generate a Meta instance from the lark Metadata.""" return Meta( empty=lark_meta.empty, column=getattr(lark_meta, "column", None), @@ -213,7 +233,27 @@ def meta_field(): return dataclasses.field(default=None, repr=False, compare=False) +def get_grammar_for_class(cls: type) -> Dict[str, str]: + """ + Given a class, get blark's ``iec.lark`` associated grammar definition(s). + """ + matches = {} + for rule, othercls in _rule_to_class.items(): + if othercls is cls: + matches[rule] = "unknown" + + if not matches: + return matches + + for rule in list(matches): + matches[rule] = util.get_grammar_for_rule(rule) + + return matches + + class _FlagHelper: + """A helper base class which translates tokens to ``enum.Flag`` instances.""" + @classmethod def from_lark(cls, token: lark.Token, *tokens: lark.Token): result = cls[token.lower()] @@ -229,6 +269,106 @@ def __str__(self): ) +@dataclasses.dataclass(frozen=True) +class TypeInformation: + """Type information derived from a specification or initialization.""" + + base_type_name: Union[str, lark.Token] + full_type_name: Union[str, lark.Token] + context: Any + + @classmethod + def from_init( + cls: Type[Self], + init: Union[ + StructureInitialization, + ArrayTypeInitialization, + StringTypeInitialization, + TypeInitialization, + SubrangeTypeInitialization, + EnumeratedTypeInitialization, + InitializedStructure, + FunctionCall, + ], + ) -> Self: + if isinstance(init, StructureInitialization): + return UnresolvedTypeInformation( # TODO + base_type_name="", + full_type_name="", + context=init, + ) + if isinstance(init, InitializedStructure): + return cls( + base_type_name=init.name, + full_type_name=init.name, + context=init, + ) + if isinstance(init, FunctionCall): + # May be multi-element variable referenve; stringified here. + return cls( + base_type_name=str(init.name), + full_type_name=str(init.name), + context=init, + ) + spec_type = cls.from_spec(init.spec) + if isinstance(init, TypeInitialization): + full_type_name = join_if(init.indirection, " ", spec_type.full_type_name) + return cls( + base_type_name=spec_type.base_type_name, + full_type_name=full_type_name, + context=init, + ) + return spec_type + + @classmethod + def from_spec( + cls: Type[Self], + spec: Union[ + ArraySpecification, + DataType, + EnumeratedSpecification, + FunctionCall, + IndirectSimpleSpecification, + ObjectInitializerArray, + SimpleSpecification, + StringTypeSpecification, + SubrangeSpecification, + ], + ) -> Self: + full_type_name = str(spec) + if isinstance(spec, DataType): + if isinstance(spec.type_name, StringTypeSpecification): + base_type_name = str(spec) + else: + base_type_name = spec.type_name + elif isinstance(spec, ArraySpecification): + base_type_name = spec.base_type_name + elif isinstance(spec, StringTypeSpecification): + base_type_name = spec.base_type_name + elif isinstance(spec, EnumeratedSpecification): + base_type_name = str(spec.type_name or spec._implicit_type_default_) + full_type_name = base_type_name + elif isinstance(spec, (SimpleSpecification, IndirectSimpleSpecification)): + base_type_name = str(spec.type) + elif isinstance(spec, SubrangeSpecification): + base_type_name = str(spec.type_name) + elif isinstance(spec, FunctionCall): + base_type_name = spec.base_type_name + else: + # base_type_name = str(spec.name) + raise NotImplementedError(spec) + return cls( + base_type_name=base_type_name, + full_type_name=full_type_name, + context=spec, + ) + + +@dataclasses.dataclass(frozen=True) +class UnresolvedTypeInformation(TypeInformation): + ... + + @_rule_handler("variable_attributes") class VariableAttributes(_FlagHelper, enum.IntFlag): constant = 0b0000_0001 @@ -261,7 +401,15 @@ class AccessSpecifier(_FlagHelper, enum.IntFlag): @dataclass @as_tagged_union class Expression: - ... + """ + Base class for all types of expressions. + + This includes all literals (integers, etc.) and more complicated + mathematical expressions. + + Marked as a "tagged union" so that serialization will uniquely identify the + Python class. + """ def __str__(self) -> str: raise NotImplementedError @@ -269,7 +417,12 @@ def __str__(self) -> str: @as_tagged_union class Literal(Expression): - """Literal value.""" + """ + Base class for all literal values. + + Marked as a "tagged union" so that serialization will uniquely identify the + Python class. + """ @dataclass @@ -356,9 +509,13 @@ def __str__(self) -> str: class BitString(Literal): """Bit string literal value.""" + #: The optional type name of the string. type_name: Optional[lark.Token] + #: The string literal. value: lark.Token + #: The numeric base of the value (e.g., 10 is decimal) base: ClassVar[int] = 10 + #: Lark metadata. meta: Optional[Meta] = meta_field() @classmethod @@ -650,6 +807,20 @@ def __str__(self) -> str: @as_tagged_union class Variable(Expression): + """ + Variable base class. + + Marked as a "tagged union" so that serialization will uniquely identify the + Python class. + + Includes: + + 1. Direct variables with I/O linkage (e.g., ``var AT %I*``); may be + located (e.g., ``AT %IX1.1``) or incomplete (e.g., just ``%I*``). + 2. "Simple", single-element variables (referenced by name, potentially + dereferenced pointers) (e.g., ``var`` or ``var^``). + 3. Multi-element variables (e.g., ``a.b.c`` or ``a^.b[1].c``). + """ ... @@ -660,7 +831,9 @@ class Variable(Expression): ) class IndirectionType: """Indirect access through a pointer or reference.""" + #: A depth of 1 is "POINTER TO", a depth of 2 is "POINTER TO POINTER TO". pointer_depth: int + #: If set, "REFERENCE TO POINTER TO..." reference: bool meta: Optional[Meta] = meta_field() @@ -697,8 +870,11 @@ def __str__(self): class IncompleteLocation(Enum): """Incomplete location information.""" none = enum.auto() + #: I/O to PLC task. input = "%I*" + #: PLC task to I/O. output = "%Q*" + #: Memory. memory = "%M*" @staticmethod @@ -712,7 +888,9 @@ def __str__(self): class VariableLocationPrefix(str, Enum): + #: I/O to PLC task. input = "I" + #: PLC task to I/O. output = "Q" memory = "M" @@ -721,6 +899,7 @@ def __str__(self) -> str: class VariableSizePrefix(str, Enum): + """Size prefix, used in locations (e.g., ``%IX1.1`` has a bit prefix).""" bit = "X" byte = "B" word_16 = "W" @@ -734,10 +913,23 @@ def __str__(self) -> str: @dataclass @_rule_handler("direct_variable") class DirectVariable(Variable): + """ + Direct variables with I/O linkage. + + Example: ``var AT %I*`` + + May be located (e.g., ``AT %IX1.1``) or incomplete (e.g., just ``%I*``). + """ + + #: The location prefix (e.g., I, Q, or M) location_prefix: VariableLocationPrefix + #: The location number itself (e.g., 2 of %IX2.1) location: lark.Token + #: Size prefix, used in locations (e.g., ``%IX1.1`` has a bit prefix). size_prefix: VariableSizePrefix + #: The number of bits. bits: Optional[List[lark.Token]] = None + #: Lark metadata. meta: Optional[Meta] = meta_field() @staticmethod @@ -764,6 +956,8 @@ def __str__(self) -> str: @_rule_handler("location") class Location(DirectVariable): + """A located direct variable. (e.g., ``AT %IX1.1``)""" + @staticmethod def from_lark(var: DirectVariable) -> Location: return Location( @@ -782,6 +976,15 @@ def __str__(self) -> str: @dataclass @_rule_handler("variable_name") class SimpleVariable(Variable): + """ + A simple, single-element variable. + + Specified by name, may potentially be dereferenced pointers. + Examples:: + + var + var^ + """ name: lark.Token dereferenced: bool meta: Optional[Meta] = meta_field() @@ -802,6 +1005,15 @@ def __str__(self) -> str: @dataclass @_rule_handler("subscript_list") class SubscriptList: + """ + A list of subscripts. + + Examples:: + + [1, 2, 3] + [Constant, GVL.Value, 1 + 3] + [1]^ + """ subscripts: List[Expression] dereferenced: bool meta: Optional[Meta] = meta_field() @@ -822,6 +1034,14 @@ def __str__(self) -> str: @dataclass @_rule_handler("field_selector") class FieldSelector: + """ + Field - or attribute - selector as part of a multi-element variable. + + Examples:: + + .field + .field^ + """ field: SimpleVariable dereferenced: bool meta: Optional[Meta] = meta_field() @@ -840,8 +1060,23 @@ def __str__(self) -> str: @dataclass @_rule_handler("multi_element_variable") class MultiElementVariable(Variable): + """ + A multi-element variable - with one or more subscripts and fields. + + Examples:: + + a.b.c + a^.b[1].c + a.b[SomeConstant].c^ + + Where ``a`` is the "name" + """ + #: The first part of the variable name. name: SimpleVariable + #: This is unused (TODO / perhaps for compat elsewhere?) + #: Dereference status is held on a per-element basis. dereferenced: bool + #: The subscripts/fields that make up the multi-element variable. elements: List[Union[SubscriptList, FieldSelector]] meta: Optional[Meta] = meta_field() @@ -863,44 +1098,108 @@ def __str__(self) -> str: SymbolicVariable = Union[SimpleVariable, MultiElementVariable] -@dataclass -@_rule_handler("simple_spec_init") -class TypeInitialization: - indirection: Optional[IndirectionType] - spec: SimpleSpecification - value: Optional[Expression] - meta: Optional[Meta] = meta_field() +class TypeInitializationBase: + """ + Base class for type initializations. + """ @property - def base_type_name(self) -> lark.Token: + def type_info(self) -> TypeInformation: """The base type name.""" - return self.spec.type + return TypeInformation.from_init(self) @property - def full_type_name(self) -> str: + def base_type_name(self) -> Union[lark.Token, str]: + """The base type name.""" + return self.type_info.base_type_name + + @property + def full_type_name(self) -> Union[lark.Token, str]: + """The full, qualified type name.""" + return self.type_info.full_type_name + + +class TypeSpecificationBase: + """ + Base class for a specification of a type. + + Can specify a: + + 1. Enumeration:: + + ( 1, 1 ) INT + TYPE_NAME (TODO; ambiguous with 2) + + 2. A simple or string type specification:: + + TYPE_NAME + STRING + STRING[255] + + 3. An indirect simple specification:: + + POINTER TO TYPE_NAME + REFERENCE TO TYPE_NAME + REFERENCE TO POINTER TO TYPE_NAME + + 4. An array specification:: + + ARRAY [1..2] OF TypeName + ARRAY [1..2] OF TypeName(1, 2) + """ + @property + def type_info(self) -> TypeInformation: + """The base type name.""" + return TypeInformation.from_spec(self) + + @property + def base_type_name(self) -> Union[lark.Token, str]: """The full type name.""" - return join_if(self.indirection, " ", self.spec.type) + return self.type_info.base_type_name - def __str__(self) -> str: - return join_if(self.full_type_name, " := ", self.value) + @property + def full_type_name(self) -> Union[lark.Token, str]: + """The full type name.""" + return self.type_info.full_type_name -class Declaration: - variables: List[DeclaredVariable] - items: List[Any] - meta: Optional[Meta] - init: Union[ - VariableInitDeclaration, - InputOutputDeclaration, - OutputDeclaration, - InputDeclaration, - GlobalVariableDeclarationType, - ] +@dataclass +@_rule_handler("simple_spec_init") +class TypeInitialization(TypeInitializationBase): + """ + A simple initialization specification of a type name. + + Example:: + + TypeName := Value1 + STRING[100] := "value" + """ + indirection: Optional[IndirectionType] + spec: SimpleSpecification + value: Optional[Expression] + meta: Optional[Meta] = meta_field() + + def __str__(self) -> str: + return join_if(self.full_type_name, " := ", self.value) @dataclass @_rule_handler("simple_type_declaration") class SimpleTypeDeclaration: + """ + A declaration of a simple type. + + Examples:: + + TypeName : INT + TypeName : INT := 5 + TypeName : INT := 5 + 1 * (2) + TypeName : REFERENCE TO INT + TypeName : POINTER TO INT + TypeName : POINTER TO POINTER TO INT + TypeName : REFERENCE TO POINTER TO INT + TypeName EXTENDS a.b : POINTER TO INT + """ name: lark.Token extends: Optional[Extends] init: TypeInitialization @@ -915,6 +1214,16 @@ def __str__(self) -> str: @dataclass @_rule_handler("string_type_declaration") class StringTypeDeclaration: + """ + A string type declaration. + + Examples:: + TypeName : STRING + TypeName : STRING := 'literal' + TypeName : STRING[5] + TypeName : STRING[100] := 'literal' + TypeName : WSTRING[100] := "literal" + """ name: lark.Token string_type: StringTypeSpecification value: Optional[String] @@ -931,7 +1240,22 @@ def __str__(self) -> str: @dataclass @_rule_handler("string_type_specification") -class StringTypeSpecification: +class StringTypeSpecification(TypeSpecificationBase): + """ + Specification of a string type. + + Examples:: + + STRING(2_500_000) + STRING(Param.iLower) + STRING(Param.iLower * 2 + 10) + STRING(Param.iLower / 2 + 10) + + Bracketed versions are also acceptable:: + + STRING[2_500_000] + STRING[Param.iLower] + """ type_name: lark.Token length: Optional[StringSpecLength] = None meta: Optional[Meta] = meta_field() @@ -955,21 +1279,22 @@ def __str__(self) -> str: "single_byte_string_spec", "double_byte_string_spec", ) -class StringTypeInitialization: +class StringTypeInitialization(TypeInitializationBase): + """ + Single or double-byte string specification. + + Examples:: + + STRING := 'test' + STRING(2_500_000) := 'test' + STRING(Param.iLower) := 'test' + + Bracketed versions are also acceptable. + """ spec: StringTypeSpecification value: Optional[lark.Token] meta: Optional[Meta] = meta_field() - @property - def base_type_name(self) -> lark.Token: - """The base type name.""" - return self.spec.base_type_name - - @property - def full_type_name(self) -> str: - """The full type name.""" - return self.spec.full_type_name - @staticmethod def from_lark( string_type: lark.Token, @@ -986,11 +1311,25 @@ def __str__(self) -> str: @dataclass @as_tagged_union class Subrange: + """ + Subrange base class. + + May be a full or partial sub-range. Marked as a "tagged union" so that + serialization will uniquely identify the Python class. + """ ... @dataclass class FullSubrange(Subrange): + """ + A full subrange (i.e., asterisk ``*``). + + Example:: + + Array[*] + ^ + """ meta: Optional[Meta] = meta_field() def __str__(self) -> str: @@ -1000,6 +1339,14 @@ def __str__(self) -> str: @dataclass @_rule_handler("subrange") class PartialSubrange(Subrange): + """ + A partial subrange, including a start/stop element index. + + Examples:: + + 1..2 + iStart..iEnd + """ start: Expression stop: Expression meta: Optional[Meta] = meta_field() @@ -1010,7 +1357,16 @@ def __str__(self) -> str: @dataclass @_rule_handler("subrange_specification") -class SubrangeSpecification: +class SubrangeSpecification(TypeSpecificationBase): + """ + A subrange specification. + + Examples:: + + INT (*) + INT (1..2) + TYPE_NAME (TODO; overlap) + """ type_name: lark.Token subrange: Optional[Subrange] = None meta: Optional[Meta] = meta_field() @@ -1033,22 +1389,20 @@ def __str__(self) -> str: @dataclass @_rule_handler("subrange_spec_init") -class SubrangeTypeInitialization: +class SubrangeTypeInitialization(TypeInitializationBase): + """ + A subrange type initialization. + + Examples:: + + INT (1..2) := 25 + """ + # TODO: coverage + examples? indirection: Optional[IndirectionType] spec: SubrangeSpecification value: Optional[Expression] = None meta: Optional[Meta] = meta_field() - @property - def base_type_name(self) -> lark.Token: - """The base type name.""" - return self.spec.base_type_name - - @property - def full_type_name(self) -> str: - """The full type name.""" - return self.spec.full_type_name - def __str__(self) -> str: spec = join_if(self.indirection, " ", self.spec) if not self.value: @@ -1060,6 +1414,14 @@ def __str__(self) -> str: @dataclass @_rule_handler("subrange_type_declaration") class SubrangeTypeDeclaration: + """ + A subrange type declaration. + + Examples:: + + TypeName : INT (1..2) + TypeName : INT (*) := 1 + """ name: lark.Token init: SubrangeTypeInitialization meta: Optional[Meta] = meta_field() @@ -1071,6 +1433,17 @@ def __str__(self) -> str: @dataclass @_rule_handler("enumerated_value") class EnumeratedValue: + """ + An enumerated value. + + Examples:: + + IdentifierB + IdentifierB := 1 + INT#IdentifierB + INT#IdentifierB := 1 + """ + # TODO: coverage? type_name: Optional[lark.Token] name: lark.Token value: Optional[Union[Integer, lark.Token]] @@ -1083,22 +1456,21 @@ def __str__(self) -> str: @dataclass @_rule_handler("enumerated_specification") -class EnumeratedSpecification: +class EnumeratedSpecification(TypeSpecificationBase): + """ + An enumerated specification. + + Examples:: + + (Value1, Value2 := 1) + (Value1, Value2 := 1) INT + INT + """ _implicit_type_default_: ClassVar[str] = "INT" type_name: Optional[lark.Token] values: Optional[List[EnumeratedValue]] = None meta: Optional[Meta] = meta_field() - @property - def base_type_name(self) -> Union[lark.Token, str]: - """The full type name.""" - return self.type_name or self._implicit_type_default_ - - @property - def full_type_name(self) -> Union[lark.Token, str]: - """The full type name.""" - return self.base_type_name - @staticmethod def from_lark(*args): if len(args) == 1: @@ -1116,22 +1488,24 @@ def __str__(self) -> str: @dataclass @_rule_handler("enumerated_spec_init") -class EnumeratedTypeInitialization: +class EnumeratedTypeInitialization(TypeInitializationBase): + """ + Enumerated specification with initialization enumerated value. + + May be indirect (i.e., POINTER TO). + + Examples:: + + (Value1, Value2 := 1) := IdentifierB + (Value1, Value2 := 1) INT := IdentifierC + INT := IdentifierD + """ + # TODO coverage + double-check examples (doctest-like?) indirection: Optional[IndirectionType] spec: EnumeratedSpecification value: Optional[EnumeratedValue] meta: Optional[Meta] = meta_field() - @property - def base_type_name(self) -> Union[lark.Token, str]: - """The base type name.""" - return self.spec.base_type_name - - @property - def full_type_name(self) -> Union[lark.Token, str]: - """The full type name.""" - return self.spec.full_type_name - def __str__(self) -> str: spec = join_if(self.indirection, " ", self.spec) return join_if(spec, " := ", self.value) @@ -1140,6 +1514,15 @@ def __str__(self) -> str: @dataclass @_rule_handler("enumerated_type_declaration", comments=True) class EnumeratedTypeDeclaration: + """ + An enumerated type declaration. + + Examples:: + + TypeName : TypeName := Va + TypeName : (Value1 := 1, Value2 := 2) + TypeName : (Value1 := 1, Value2 := 2) INT := Value1 + """ name: lark.Token init: EnumeratedTypeInitialization meta: Optional[Meta] = meta_field() @@ -1151,6 +1534,15 @@ def __str__(self) -> str: @dataclass @_rule_handler("non_generic_type_name") class DataType: + """ + A non-generic type name, or a data type name. + + May be indirect (e.g., POINTER TO). + + An elementary type name, a derived type name, or a general dotted + identifier are valid for this. + """ + # TODO: more grammar overlaps with dotted/simple names? indirection: Optional[IndirectionType] type_name: Union[lark.Token, StringTypeSpecification] meta: Optional[Meta] = meta_field() @@ -1163,7 +1555,13 @@ def __str__(self) -> str: @dataclass @_rule_handler("simple_specification") -class SimpleSpecification: +class SimpleSpecification(TypeSpecificationBase): + """ + A simple specification with just a type name (or a string type name). + + An elementary type name, a simple type name, or a general dotted + identifier are valid for this. + """ type: Union[lark.Token, StringTypeSpecification] meta: Optional[Meta] = meta_field() @@ -1173,7 +1571,17 @@ def __str__(self) -> str: @dataclass @_rule_handler("indirect_simple_specification") -class IndirectSimpleSpecification: +class IndirectSimpleSpecification(TypeSpecificationBase): + """ + A simple specification with the possibility of indirection. + + Examples:: + + TypeName + POINTER TO TypeName + REFERENCE TO TypeName + REFERENCE TO POINTER TO TypeName + """ indirection: Optional[IndirectionType] type: SimpleSpecification meta: Optional[Meta] = meta_field() @@ -1194,9 +1602,21 @@ def __str__(self) -> str: @dataclass @_rule_handler("array_specification") -class ArraySpecification: - type: ArraySpecType +class ArraySpecification(TypeSpecificationBase): + """ + An array specification. + + Examples:: + + ARRAY[*] OF TypeName + ARRAY[1..2] OF Call(1, 2) + ARRAY[1..2] OF Call(1, 2) + ARRAY[1..5] OF Vec(SIZEOF(TestStruct), 0) + ARRAY[1..5] OF STRING[10] + ARRAY[1..5] OF STRING(Param.iLower) + """ subranges: List[Subrange] + type: ArraySpecType meta: Optional[Meta] = meta_field() @property @@ -1237,6 +1657,21 @@ def __str__(self) -> str: @dataclass @_rule_handler("array_initial_element") class ArrayInitialElement: + """ + Initial value for an array element (potentialy repeated). + + The element itself may be an expression, a structure initialization, an + enumerated value, or an array initialization. + + It may have a repeat value (``count``) as in:: + + Repeat(Value) + 10(5) + Repeat(5 + 3) + INT#IdentifierB(5 + 3) + """ + # NOTE: order is correct here; see rule array_initial_element_count + # TODO: check enumerated value for count? specifically the := one element: ArrayInitialElementType count: Optional[Union[EnumeratedValue, Integer]] = None meta: Optional[Meta] = meta_field() @@ -1249,6 +1684,10 @@ def __str__(self) -> str: @_rule_handler("array_initial_element_count") class _ArrayInitialElementCount: + """ + An internal handler for array initial elements with repeat count + values. + """ @staticmethod def from_lark( count: Union[EnumeratedValue, Integer], @@ -1263,6 +1702,11 @@ def from_lark( @dataclass @_rule_handler("bracketed_array_initialization") class _BracketedArrayInitialization: + """ + Internal handler for array initialization with brackets. + + See also :class:`ArrayInitialization` + """ @staticmethod def from_lark(*elements: ArrayInitialElement) -> ArrayInitialization: return ArrayInitialization(list(elements), brackets=True) @@ -1271,6 +1715,11 @@ def from_lark(*elements: ArrayInitialElement) -> ArrayInitialization: @dataclass @_rule_handler("bare_array_initialization") class _BareArrayInitialization: + """ + Internal handler for array initialization, without brackets + + See also :class:`ArrayInitialization` + """ @staticmethod def from_lark(*elements: ArrayInitialElement) -> ArrayInitialization: return ArrayInitialization(list(elements), brackets=False) @@ -1278,6 +1727,14 @@ def from_lark(*elements: ArrayInitialElement) -> ArrayInitialization: @dataclass class ArrayInitialization: + """ + Array initialization (bare or bracketed). + + Examples:: + + [1, 2, 3] + 1, 2, 3 + """ elements: List[ArrayInitialElement] brackets: bool = False meta: Optional[Meta] = meta_field() @@ -1292,6 +1749,13 @@ def __str__(self) -> str: @dataclass @_rule_handler("object_initializer_array") class ObjectInitializerArray: + """ + Object initialization in array form. + + Examples:: + + FB_Runner[(name := 'one'), (name := 'two')] + """ name: lark.Token initializers: List[StructureInitialization] meta: Optional[Meta] = meta_field() @@ -1316,22 +1780,23 @@ def __str__(self) -> str: @dataclass @_rule_handler("array_spec_init") -class ArrayTypeInitialization: +class ArrayTypeInitialization(TypeInitializationBase): + """ + Array specification and optional default (initialization) value. + + May be indirect (e.g., POINTER TO). + + Examples:: + + ARRAY[*] OF TypeName + ARRAY[1..2] OF Call(1, 2) := [1, 2] + POINTER TO ARRAY[1..2] OF Call(1, 2) + """ indirection: Optional[IndirectionType] spec: ArraySpecification value: Optional[ArrayInitialization] meta: Optional[Meta] = meta_field() - @property - def base_type_name(self) -> Union[str, lark.Token]: - """The base type name.""" - return self.spec.base_type_name - - @property - def full_type_name(self) -> str: - """The full type name.""" - return self.spec.full_type_name - def __str__(self) -> str: if self.indirection: spec = f"{self.indirection} {self.spec}" @@ -1347,6 +1812,22 @@ def __str__(self) -> str: @dataclass @_rule_handler("array_type_declaration", comments=True) class ArrayTypeDeclaration: + """ + Full declaration of an array type. + + Examples:: + + ArrayType : ARRAY[*] OF TypeName + ArrayType : ARRAY[1..2] OF Call(1, 2) := [1, 2] + ArrayType : POINTER TO ARRAY[1..2] OF Call(1, 2) + TypeName : ARRAY [1..2, 3..4] OF INT + TypeName : ARRAY [1..2] OF INT := [1, 2] + TypeName : ARRAY [1..2, 3..4] OF INT := [2(3), 3(4)] + TypeName : ARRAY [1..2, 3..4] OF Tc.SomeType + TypeName : ARRAY [1..2, 3..4] OF Tc.SomeType(someInput := 3) + TypeName : ARRAY [1..2, 3..4] OF ARRAY [1..2] OF INT + TypeName : ARRAY [1..2, 3..4] OF ARRAY [1..2] OF ARRAY [3..4] OF INT + """ name: lark.Token init: ArrayTypeInitialization meta: Optional[Meta] = meta_field() @@ -1358,6 +1839,34 @@ def __str__(self) -> str: @dataclass @_rule_handler("structure_type_declaration", comments=True) class StructureTypeDeclaration: + """ + Full structure type declaration, as part of a TYPE. + + Examples:: + + TypeName EXTENDS Other.Type : + STRUCT + iValue : INT; + END_STRUCT + + TypeName : POINTER TO + STRUCT + iValue : INT; + END_STRUCT + + TypeName : POINTER TO + STRUCT + iValue : INT := 3 + 4; + stTest : ST_Testing := (1, 2); + eValue : E_Test := E_Test.ABC; + arrValue : ARRAY [1..2] OF INT := [1, 2]; + arrValue1 : INT (1..2); + arrValue1 : (Value1 := 1) INT; + sValue : STRING := 'abc'; + iValue1 AT %I* : INT := 5; + sValue1 : STRING[10] := 'test'; + END_STRUCT + """ name: lark.Token extends: Optional[Extends] indirection: Optional[IndirectionType] @@ -1398,6 +1907,21 @@ def __str__(self) -> str: @dataclass @_rule_handler("structure_element_declaration", comments=True) class StructureElementDeclaration: + """ + Declaration of a single element of a structure. + + Examples:: + + iValue : INT := 3 + 4; + stTest : ST_Testing := (1, 2); + eValue : E_Test := E_Test.ABC; + arrValue : ARRAY [1..2] OF INT := [1, 2]; + arrValue1 : INT (1..2); + arrValue1 : (Value1 := 1) INT; + sValue : STRING := 'abc'; + iValue1 AT %I* : INT := 5; + sValue1 : STRING[10] := 'test'; + """ name: lark.Token location: Optional[IncompleteLocation] init: Union[ @@ -1414,9 +1938,30 @@ class StructureElementDeclaration: @property def variables(self) -> List[str]: - """API compat""" + """API compat: list of variable names.""" return [self.name] + @property + def value(self) -> str: + """The initialization value, if applicable.""" + if isinstance(self.init, StructureInitialization): + return str(self.init) + return str(self.init.value) + + @property + def base_type_name(self) -> Union[lark.Token, str]: + """The base type name.""" + if isinstance(self.init, StructureInitialization): + return self.name + return self.init.base_type_name + + @property + def full_type_name(self) -> lark.Token: + """The full type name.""" + if isinstance(self.init, StructureInitialization): + return self.name + return self.init.full_type_name + def __str__(self) -> str: name_and_location = join_if(self.name, " ", self.location) return f"{name_and_location} : {self.init};" @@ -1434,6 +1979,21 @@ def __str__(self) -> str: @dataclass @_rule_handler("union_element_declaration", comments=True) class UnionElementDeclaration: + """ + Declaration of a single element of a union. + + Similar to a structure element, but not all types are supported and no + initialization/default values are allowed. + + Examples:: + + iValue : INT; + arrValue : ARRAY [1..2] OF INT; + arrValue1 : INT (1..2); + arrValue1 : (Value1 := 1) INT; + sValue : STRING; + psValue1 : POINTER TO STRING[10]; + """ name: lark.Token spec: UnionElementSpecification meta: Optional[Meta] = meta_field() @@ -1450,6 +2010,21 @@ def __str__(self) -> str: @dataclass @_rule_handler("union_type_declaration", comments=True) class UnionTypeDeclaration: + """ + A full declaration of a UNION type, as part of a TYPE/END_TYPE block. + + Examples:: + + UNION + iVal : INT; + aAsBytes : ARRAY [0..2] OF BYTE; + END_UNION + + UNION + iValue : INT; + eValue : (iValue := 1, iValue2 := 2) INT; + END_UNION + """ name: lark.Token declarations: List[UnionElementDeclaration] meta: Optional[Meta] = meta_field() @@ -1484,7 +2059,14 @@ def __str__(self) -> str: @dataclass @_rule_handler("initialized_structure") -class InitializedStructure: +class InitializedStructure(TypeInitializationBase): + """ + A named initialized structure. + + Examples:: + + ST_TypeName := (iValue := 0, bValue := TRUE) + """ name: lark.Token init: StructureInitialization meta: Optional[Meta] = meta_field() @@ -1494,16 +2076,6 @@ def value(self) -> str: """The initialization value (call).""" return str(self.init) - @property - def base_type_name(self) -> lark.Token: - """The base type name.""" - return self.name - - @property - def full_type_name(self) -> lark.Token: - """The full type name.""" - return self.name - def __str__(self) -> str: return f"{self.name} := {self.init}" @@ -1511,6 +2083,28 @@ def __str__(self) -> str: @dataclass @_rule_handler("structure_initialization") class StructureInitialization: + """ + A structure initialization (i.e., default values) of one or more elements. + + Elements may be either positional or named. Used in the following: + + 1. Structure element initialization of default values:: + + stStruct : ST_TypeName := (iValue := 0, bValue := TRUE) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + 2. Function block declarations (fb_name_decl, fb_invocation_decl):: + + fbSample : FB_Sample(nInitParam := 1) := (nInput := 2, nMyProperty := 3) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + fbSample : FB_Sample := (nInput := 2, nMyProperty := 3) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + 3. Array object initializers (object_initializer_array):: + + runners : ARRAY[1..2] OF FB_Runner[(name := 'one'), (name := 'two')] + [^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^] + """ elements: List[StructureElementInitialization] meta: Optional[Meta] = meta_field() @@ -1526,6 +2120,20 @@ def __str__(self) -> str: @dataclass @_rule_handler("structure_element_initialization") class StructureElementInitialization: + """ + An initialization (default) value for a structure element. + + This may come in the form of:: + + name := value + + or simply:: + + value + + ``value`` may refer to an expression, an enumerated value, represent + a whole array, or represent a nested structure. + """ name: Optional[lark.Token] value: Union[ Constant, @@ -1554,6 +2162,7 @@ def __str__(self) -> str: @dataclass @_rule_handler("unary_expression") class UnaryOperation(Expression): + """A unary - single operand - operation: ``NOT``, ``-``, or ``+``.""" op: lark.Token expr: Expression meta: Optional[Meta] = meta_field() @@ -1587,6 +2196,23 @@ def __str__(self) -> str: "expression_term" ) class BinaryOperation(Expression): + """ + A binary (i.e., two operand) operation. + + Examples:: + + a + b + a AND b + a AND_THEN b + a OR_ELSE b + a := b + a XOR b + a = b + -a * b + a * 1.0 + + Expressions may be nested in either the left or right operand. + """ left: Expression op: lark.Token right: Expression @@ -1625,6 +2251,14 @@ def __str__(self): @dataclass @_rule_handler("parenthesized_expression") class ParenthesizedExpression(Expression): + """ + An expression with parentheses around it. + + Examples:: + + (a * b) + (1 + b) + """ expr: Expression meta: Optional[Meta] = meta_field() @@ -1635,6 +2269,18 @@ def __str__(self) -> str: @dataclass @_rule_handler("bracketed_expression") class BracketedExpression(Expression): + """ + An expression with square brackets around it. + + This is used exclusively in string length specifications. + + Examples:: + + [a * b] + [255] + + See also :class:`StringSpecLength`. + """ expression: Expression meta: Optional[Meta] = meta_field() @@ -1645,6 +2291,19 @@ def __str__(self) -> str: @dataclass @_rule_handler("string_spec_length") class StringSpecLength: + """ + The length of a defined string. + + The grammar makes a distinction between brackets and parentheses, though + they appear to be functionally equivalent. + + Examples:: + + [1] + (1) + [255] + """ + length: Union[ParenthesizedExpression, BracketedExpression] def __str__(self) -> str: @@ -1654,8 +2313,23 @@ def __str__(self) -> str: @dataclass @_rule_handler("function_call") class FunctionCall(Expression): + """ + A function (function block, method, action, etc.) call. + + The return value may be dereferenced with a carat (``^``). + + Examples:: + + A()^ + A(1, 2) + A(1, 2, sName:='test', iOutput=>) + A.B[1].C(1, 2) + """ + #: The function name. name: SymbolicVariable + #: Positional, naed, or output parameters. parameters: List[ParameterAssignment] + #: Dereference the return value? dereferenced: bool meta: Optional[Meta] = meta_field() @@ -1714,6 +2388,16 @@ def __str__(self) -> str: @dataclass @_rule_handler("var1") class DeclaredVariable: + """ + A single declared variable name and optional [direct or incomplete] location. + + Examples:: + + iVar + iVar AT %I* + iVar AT %IX1.1 + """ + # Alternate name: VariableWithLocation? MaybeLocatedVariable? variable: SimpleVariable location: Optional[Union[IncompleteLocation, Location]] @@ -1757,6 +2441,9 @@ def __str__(self) -> str: class InitDeclaration: + """ + Base class for a declaration of one or more variables with a type initialization. + """ variables: List[DeclaredVariable] init: InitDeclarationType meta: Optional[Meta] @@ -1769,6 +2456,19 @@ def __str__(self) -> str: @dataclass @_rule_handler("var1_init_decl", comments=True) class VariableOneInitDeclaration(InitDeclaration): + """ + A declaration of one or more variables with a type, subrange, or enumerated + type initialization. + + Examples:: + + stVar1, stVar2 : (Value1, Value2) + stVar1, stVar2 : (Value1 := 0, Value2 := 1) + stVar1 : INT (1..2) := 25 + stVar1, stVar2 : TypeName := Value + stVar1, stVar2 : (Value1 := 1, Value2 := 2) + stVar1, stVar2 : (Value1 := 1, Value2 := 2) INT := Value1 + """ variables: List[DeclaredVariable] init: Union[TypeInitialization, SubrangeTypeInitialization, EnumeratedTypeInitialization] meta: Optional[Meta] = meta_field() @@ -1777,6 +2477,18 @@ class VariableOneInitDeclaration(InitDeclaration): @dataclass @_rule_handler("array_var_init_decl", comments=True) class ArrayVariableInitDeclaration(InitDeclaration): + """ + A declaration of one or more variables with array type initialization and + optional default (initialization) value. + + May be indirect (e.g., POINTER TO). + + Examples:: + + aVal1, aVal2 : ARRAY[*] OF TypeName + aVal1 : ARRAY[1..2] OF Call(1, 2) := [1, 2] + aVal1 : POINTER TO ARRAY[1..2] OF Call(1, 2) + """ variables: List[DeclaredVariable] init: ArrayTypeInitialization meta: Optional[Meta] = meta_field() @@ -1785,6 +2497,14 @@ class ArrayVariableInitDeclaration(InitDeclaration): @dataclass @_rule_handler("structured_var_init_decl", comments=True) class StructuredVariableInitDeclaration(InitDeclaration): + """ + A declaration of one or more variables using a named initialized structure. + + Examples:: + + stVar1 : ST_TypeName := (iValue := 0, bValue := TRUE) + stVar1, stVar2 : ST_TypeName := (iValue = 0, bValue := TRUE) + """ variables: List[DeclaredVariable] init: InitializedStructure meta: Optional[Meta] = meta_field() @@ -1797,6 +2517,16 @@ class StructuredVariableInitDeclaration(InitDeclaration): comments=True ) class StringVariableInitDeclaration(InitDeclaration): + """ + A declaration of one or more variables using single/double byte strings, + with an optinoal initialization value. + + Examples:: + + sVar1 : STRING(2_500_000) := 'test1' + sVar2, sVar3 : STRING(Param.iLower) := 'test2' + sVar4, sVar5 : WSTRING(Param.iLower) := "test3" + """ variables: List[DeclaredVariable] spec: StringTypeSpecification value: Optional[lark.Token] @@ -1821,6 +2551,14 @@ def from_lark(variables: List[DeclaredVariable], string_info: StringTypeInitiali @dataclass @_rule_handler("edge_declaration", comments=True) class EdgeDeclaration(InitDeclaration): + """ + An edge declaration of one or more variables. + + Examples:: + + iValue AT %IX1.1 : BOOL R_EDGE + iValue : BOOL F_EDGE + """ variables: List[DeclaredVariable] edge: lark.Token meta: Optional[Meta] = meta_field() @@ -1841,12 +2579,27 @@ def __str__(self): @as_tagged_union class FunctionBlockDeclaration: + """ + Base class for declarations of variables using function blocks. + + May either be by name (:class:`FunctionBlockNameDeclaration`) or invocation + :class:`FunctionBlockInvocationDeclaration`). Marked as a "tagged union" so + that serialization will uniquely identify the Python class. + """ ... @dataclass @_rule_handler("fb_name_decl", comments=True) class FunctionBlockNameDeclaration(FunctionBlockDeclaration): + """ + Base class for declarations of variables using function blocks by name. + + Examples:: + + fbName1 : FB_Name + fbName1 : FB_Name := (iValue := 0, bValue := TRUE) + """ variables: List[lark.Token] # fb_decl_name_list -> fb_name spec: lark.Token init: Optional[StructureInitialization] = None @@ -1861,6 +2614,13 @@ def __str__(self) -> str: @dataclass @_rule_handler("fb_invocation_decl", comments=True) class FunctionBlockInvocationDeclaration(FunctionBlockDeclaration): + """ + Base class for declarations of variables using function blocks by invocation. + + Examples:: + + fbSample : FB_Sample(nInitParam := 1) := (nInput := 2, nMyProperty := 3) + """ variables: List[lark.Token] init: FunctionCall defaults: Optional[StructureInitialization] = None @@ -1874,12 +2634,28 @@ def __str__(self) -> str: @as_tagged_union class ParameterAssignment: + """ + Base class for assigned parameters in function calls. + + May be either input parameters (positional or named ``name :=``) or output + parameters (named as in ``name =>``, ``NOT name =>``). Marked as a "tagged + union" so that serialization will uniquely identify the Python class. + """ ... @dataclass @_rule_handler("param_assignment") class InputParameterAssignment(ParameterAssignment): + """ + An input parameter in a function call. + + May be a nameless positional parameter or a named one. + + Examples:: + + name := value + """ name: Optional[SimpleVariable] value: Optional[Expression] meta: Optional[Meta] = meta_field() @@ -1900,6 +2676,16 @@ def __str__(self) -> str: @dataclass @_rule_handler("output_parameter_assignment") class OutputParameterAssignment(ParameterAssignment): + """ + A named output parameter, which may be inverted. + + Examples:: + + name => output + NOT name => output2 + name => + NOT name => + """ name: SimpleVariable value: Optional[Expression] inverted: bool = False @@ -1926,6 +2712,19 @@ def __str__(self) -> str: @dataclass @_rule_handler("global_var_spec") class GlobalVariableSpec: + """ + Global variable specification; the part that comes before the + initialization. + + Located (or incomplete located) specifications only apply to one variable, + whereas simple specifications can have multiple variables. + + Examples:: + + iValue1, iValue2 + iValue3 AT %I* + iValue4 AT %IX1.1 + """ variables: List[lark.Token] location: Optional[AnyLocation] meta: Optional[Meta] = meta_field() @@ -1963,6 +2762,22 @@ def __str__(self) -> str: @dataclass @_rule_handler("global_var_decl", comments=True) class GlobalVariableDeclaration: + """ + A declaration of one or more global variables: name and location + specification and initialization type. + + Examples:: + + fValue1 : INT; + fValue2 : INT (0..10); + fValue3 : (A, B); + fValue4 : (A, B) DINT; + fValue5 : ARRAY [1..10] OF INT; + fValue6 : ARRAY [1..10] OF ARRAY [1..10] OF INT; + fValue7 : FB_Test(1, 2, 3); + fValue8 : FB_Test(A := 1, B := 2, C => 3); + fValue9 : STRING[10] := 'abc'; + """ spec: GlobalVariableSpec init: Union[LocatedVariableSpecInit, FunctionCall] meta: Optional[Meta] = meta_field() @@ -1994,6 +2809,15 @@ def __str__(self) -> str: @dataclass @_rule_handler("extends") class Extends: + """ + The "EXTENDS" portion of a function block, interface, structure, etc. + + Examples:: + + EXTENDS stName + EXTENDS FB_Name + """ + name: lark.Token meta: Optional[Meta] = meta_field() @@ -2004,6 +2828,16 @@ def __str__(self) -> str: @dataclass @_rule_handler("implements") class Implements: + """ + The "IMPLEMENTS" portion of a function block, indicating it implements + one or more interfaces. + + Examples:: + + IMPLEMENTS I_Interface1 + IMPLEMENTS I_Interface1, I_Interface2 + """ + interfaces: List[lark.Token] meta: Optional[Meta] = meta_field() @@ -2020,6 +2854,36 @@ def __str__(self) -> str: @dataclass @_rule_handler("function_block_type_declaration", comments=True) class FunctionBlock: + """ + A full function block type declaration. + + A function block distinguishes itself from a regular function by having + state and potentially having actions, methods, and properties. These + additional parts are separate in this grammar (i.e., they do not appear + within the FUNCTION_BLOCK itself). + + An implementation is optional, but ``END_FUNCTION_BLOCK`` is required. + + Examples:: + + FUNCTION_BLOCK FB_EmptyFunctionBlock + END_FUNCTION_BLOCK + + FUNCTION_BLOCK FB_Implementer IMPLEMENTS I_fbName + END_FUNCTION_BLOCK + + FUNCTION_BLOCK ABSTRACT FB_Extender EXTENDS OtherFbName + END_FUNCTION_BLOCK + + FUNCTION_BLOCK FB_WithVariables + VAR_INPUT + bExecute : BOOL; + END_VAR + VAR_OUTPUT + iResult : INT; + END_VAR + END_FUNCTION_BLOCK + """ name: lark.Token access: Optional[AccessSpecifier] extends: Optional[Extends] @@ -2067,6 +2931,20 @@ def __str__(self) -> str: @dataclass @_rule_handler("function_declaration", comments=True) class Function: + """ + A full function block type declaration, with nested variable declaration blocks. + + An implementation is optional, but ``END_FUNCTION`` is required. + + Examples:: + + FUNCTION FuncName : INT + VAR_INPUT + iValue : INT := 0; + END_VAR + FuncName := iValue; + END_FUNCTION + """ access: Optional[AccessSpecifier] name: lark.Token return_type: Optional[Union[SimpleSpecification, IndirectSimpleSpecification]] @@ -2111,6 +2989,23 @@ def __str__(self) -> str: @dataclass @_rule_handler("program_declaration", comments=True) class Program: + """ + A full program declaration, with nested variable declaration blocks. + + An implementation is optional, but ``END_PROGRAM`` is required. + + Examples:: + + PROGRAM ProgramName + VAR_INPUT + iValue : INT; + END_VAR + VAR_ACCESS + AccessName : SymbolicVariable : TypeName READ_WRITE; + END_VAR + iValue := iValue + 1; + END_PROGRAM + """ name: lark.Token declarations: List[VariableDeclarationBlock] body: Optional[FunctionBody] @@ -2140,6 +3035,15 @@ def __str__(self) -> str: @dataclass @_rule_handler("interface_declaration", comments=True) class Interface: + """ + A full interface declaration, with nested variable declaration blocks. + + An implementation is not allowed for interfaces, but ``END_INTERFACE`` is + still required. + + Examples:: + + """ name: lark.Token extends: Optional[Extends] # TODO: want this to be tagged during serialization, so it's kept as @@ -2178,6 +3082,23 @@ def __str__(self) -> str: @dataclass @_rule_handler("action", comments=True) class Action: + """ + A full, named action declaration. + + Actions belong to function blocks. Actions may not contain variable blocks, + but may contain an implementation. Variable references are assumed to be + from the local namespace (i.e., the owner function block) or in the global + scope. + + Examples:: + + ACTION ActName + END_ACTION + + ACTION ActName + iValue := iValue + 2; + END_ACTION + """ name: lark.Token body: Optional[FunctionBody] meta: Optional[Meta] = meta_field() @@ -2197,6 +3118,21 @@ def __str__(self) -> str: @dataclass @_rule_handler("function_block_method_declaration", comments=True) class Method: + """ + A full, named method declaration. + + Methods belong to function blocks. Methods may contain variable blocks + and a return type, and may also contain an implementation. + + Examples:: + + METHOD PRIVATE MethodName : ARRAY [1..2] OF INT + END_METHOD + + METHOD MethodName : INT + MethodName := 1; + END_METHOD + """ access: Optional[AccessSpecifier] name: lark.Token return_type: Optional[LocatedVariableSpecInit] @@ -2238,6 +3174,28 @@ def __str__(self) -> str: @dataclass @_rule_handler("function_block_property_declaration", comments=True) class Property: + """ + A named property declaration, which may pertain to a ``get`` or ``set``. + + Properties belong to function blocks. Properties may contain variable + blocks and a return type, and may also contain an implementation. + + Examples:: + + PROPERTY PropertyName : RETURNTYPE + VAR_INPUT + bExecute : BOOL; + END_VAR + VAR_OUTPUT + iResult : INT; + END_VAR + iResult := 5; + PropertyName := iResult + 1; + END_PROPERTY + + PROPERTY PRIVATE PropertyName : ARRAY [1..2] OF INT + END_PROPERTY + """ access: Optional[AccessSpecifier] name: lark.Token return_type: Optional[LocatedVariableSpecInit] @@ -2300,6 +3258,12 @@ def __str__(self) -> str: @as_tagged_union class VariableDeclarationBlock: + """ + Base class for variable declaration blocks. + + Marked as a "tagged union" so that serialization will uniquely identify the + Python class. + """ block_header: ClassVar[str] = "VAR" items: List[Any] meta: Optional[Meta] @@ -2314,6 +3278,11 @@ def attribute_pragmas(self) -> List[str]: @dataclass @_rule_handler("var_declarations", comments=True) class VariableDeclarations(VariableDeclarationBlock): + """ + Variable declarations block (``VAR``). + + May be annotated with attributes (see :class:`VariableAttributes`). + """ block_header: ClassVar[str] = "VAR" attrs: Optional[VariableAttributes] items: List[VariableInitDeclaration] @@ -2339,6 +3308,11 @@ def __str__(self) -> str: @dataclass @_rule_handler("static_var_declarations", comments=True) class StaticDeclarations(VariableDeclarationBlock): + """ + Static variable declarations block (``VAR_STAT``). + + May be annotated with attributes (see :class:`VariableAttributes`). + """ block_header: ClassVar[str] = "VAR_STAT" attrs: Optional[VariableAttributes] items: List[VariableInitDeclaration] @@ -2365,6 +3339,11 @@ def __str__(self) -> str: @dataclass @_rule_handler("temp_var_decls", comments=True) class TemporaryVariableDeclarations(VariableDeclarationBlock): + """ + Temporary variable declarations block (``VAR_TEMP``). + + May be annotated with attributes (see :class:`VariableAttributes`). + """ block_header: ClassVar[str] = "VAR_TEMP" items: List[VariableInitDeclaration] meta: Optional[Meta] = meta_field() @@ -2388,6 +3367,11 @@ def __str__(self) -> str: @dataclass @_rule_handler("var_inst_declaration", comments=True) class MethodInstanceVariableDeclarations(VariableDeclarationBlock): + """ + Declarations block for instance variables in methods (``VAR_INST``). + + May be annotated with attributes (see :class:`VariableAttributes`). + """ block_header: ClassVar[str] = "VAR_INST" attrs: Optional[VariableAttributes] items: List[VariableInitDeclaration] @@ -2418,6 +3402,10 @@ def __str__(self) -> str: @dataclass @_rule_handler("located_var_decl", comments=True) class LocatedVariableDeclaration: + """ + Declaration of a variable in a VAR block that is located. + """ + # TODO examples name: Optional[SimpleVariable] location: Location init: LocatedVariableSpecInit @@ -2431,6 +3419,13 @@ def __str__(self) -> str: @dataclass @_rule_handler("located_var_declarations", comments=True) class LocatedVariableDeclarations(VariableDeclarationBlock): + """ + Located variable declarations block (``VAR``). + + May be annotated with attributes (see :class:`VariableAttributes`). + + All variables in this are expected to be located (e.g., ``AT %IX1.1``). + """ block_header: ClassVar[str] = "VAR" attrs: Optional[VariableAttributes] items: List[LocatedVariableDeclaration] @@ -2456,13 +3451,11 @@ def __str__(self) -> str: ) +#: var_spec in the grammar IncompleteLocatedVariableSpecInit = Union[ SimpleSpecification, - TypeInitialization, SubrangeTypeInitialization, EnumeratedTypeInitialization, - ArrayTypeInitialization, - InitializedStructure, StringTypeSpecification, ] @@ -2470,6 +3463,9 @@ def __str__(self) -> str: @dataclass @_rule_handler("incomplete_located_var_decl", comments=True) class IncompleteLocatedVariableDeclaration: + """ + A named, incomplete located variable declaration inside a variable block. + """ name: SimpleVariable location: IncompleteLocation init: IncompleteLocatedVariableSpecInit @@ -2483,6 +3479,14 @@ def __str__(self) -> str: @dataclass @_rule_handler("incomplete_located_var_declarations", comments=True) class IncompleteLocatedVariableDeclarations(VariableDeclarationBlock): + """ + Incomplete located variable declarations block (``VAR``). + + May be annotated with attributes (see :class:`VariableAttributes`). + + All variables in this are expected to have incomplete locations (e.g., just + ``%I*``). + """ block_header: ClassVar[str] = "VAR" attrs: Optional[VariableAttributes] items: List[IncompleteLocatedVariableDeclaration] @@ -2511,6 +3515,9 @@ def __str__(self) -> str: @dataclass @_rule_handler("external_declaration", comments=True) class ExternalVariableDeclaration: + """ + A named, external variable declaration inside a variable block. + """ name: lark.Token spec: Union[ SimpleSpecification, @@ -2528,6 +3535,11 @@ def __str__(self) -> str: @dataclass @_rule_handler("external_var_declarations", comments=True) class ExternalVariableDeclarations(VariableDeclarationBlock): + """ + A block of named, external variable declarations (``VAR_EXTERNAL``). + + May be annotated with attributes (see :class:`VariableAttributes`). + """ block_header: ClassVar[str] = "VAR_EXTERNAL" attrs: Optional[VariableAttributes] items: List[ExternalVariableDeclaration] @@ -2556,6 +3568,11 @@ def __str__(self) -> str: @dataclass @_rule_handler("input_declarations", comments=True) class InputDeclarations(VariableDeclarationBlock): + """ + A block of named, input variable declarations (``VAR_INPUT``). + + May be annotated with attributes (see :class:`VariableAttributes`). + """ block_header: ClassVar[str] = "VAR_INPUT" attrs: Optional[VariableAttributes] items: List[InputDeclaration] @@ -2580,6 +3597,11 @@ def __str__(self) -> str: @dataclass @_rule_handler("output_declarations", comments=True) class OutputDeclarations(VariableDeclarationBlock): + """ + A block of named, output variable declarations (``VAR_OUTPUT``). + + May be annotated with attributes (see :class:`VariableAttributes`). + """ block_header: ClassVar[str] = "VAR_OUTPUT" attrs: Optional[VariableAttributes] items: List[OutputDeclaration] @@ -2606,6 +3628,11 @@ def __str__(self) -> str: @dataclass @_rule_handler("input_output_declarations", comments=True) class InputOutputDeclarations(VariableDeclarationBlock): + """ + A block of named, input/output variable declarations (``VAR_IN_OUT``). + + May be annotated with attributes (see :class:`VariableAttributes`). + """ block_header: ClassVar[str] = "VAR_IN_OUT" attrs: Optional[VariableAttributes] items: List[InputOutputDeclaration] @@ -2630,23 +3657,6 @@ def __str__(self) -> str: ) -@dataclass -@_rule_handler("program_access_decl", comments=True) -class AccessDeclaration: - name: lark.Token - variable: SymbolicVariable - type: DataType - direction: Optional[lark.Token] - meta: Optional[Meta] = meta_field() - - def __str__(self) -> str: - return join_if( - f"{self.name} : {self.variable} : {self.type}", - " ", - self.direction - ) - - @dataclass @_rule_handler("function_var_declarations", comments=True) class FunctionVariableDeclarations(VariableDeclarationBlock): @@ -2676,9 +3686,42 @@ def __str__(self) -> str: ) +@dataclass +@_rule_handler("program_access_decl", comments=True) +class AccessDeclaration: + """ + A single, named program access declaration. + + Examples:: + + AccessName : SymbolicVariable : TypeName READ_WRITE; + AccessName1 : SymbolicVariable1 : TypeName1 READ_ONLY; + AccessName2 : SymbolicVariable2 : TypeName2; + """ + name: lark.Token + variable: SymbolicVariable + type: DataType + direction: Optional[lark.Token] + meta: Optional[Meta] = meta_field() + + def __str__(self) -> str: + return join_if( + f"{self.name} : {self.variable} : {self.type}", + " ", + self.direction + ) + + @dataclass @_rule_handler("program_access_decls", comments=True) class AccessDeclarations(VariableDeclarationBlock): + """ + A block of named, program access variable declarations (``VAR_ACCESS``). + + See Also + -------- + :class:`AccessDeclaration` + """ block_header: ClassVar[str] = "VAR_ACCESS" items: List[AccessDeclaration] meta: Optional[Meta] = meta_field() @@ -2700,6 +3743,11 @@ def __str__(self) -> str: @dataclass @_rule_handler("global_var_declarations", comments=True) class GlobalVariableDeclarations(VariableDeclarationBlock): + """ + Global variable declarations block (``VAR_GLOBAL``). + + May be annotated with attributes (see :class:`GlobalVariableAttributes`). + """ block_header: ClassVar[str] = "VAR_GLOBAL" attrs: Optional[GlobalVariableAttributes] items: List[GlobalVariableDeclaration] @@ -2729,11 +3777,26 @@ def __str__(self) -> str: @as_tagged_union class Statement: - ... + """ + Base class for all statements in a structured text implementation section. + + Marked as a "tagged union" so that serialization will uniquely identify the + Python class. + """ @_rule_handler("function_call_statement", comments=True) class FunctionCallStatement(Statement, FunctionCall): + """ + A function (function block, method, action, etc.) call as a statement. + + Examples:: + + A(1, 2); + A(1, 2, sName:='test', iOutput=>); + A.B[1].C(1, 2); + """ + @staticmethod def from_lark( invocation: FunctionCall, @@ -2753,6 +3816,14 @@ def __str__(self): @dataclass @_rule_handler("chained_function_call_statement", comments=True) class ChainedFunctionCallStatement(Statement): + """ + A chained set of function calls as a statement, in a "fluent" style. + + Examples:: + + uut.dothis().andthenthis().andthenthat(); + uut.getPointerToStruct()^.dothis(A := 1).dothat(B := 2).done(); + """ invocations: List[FunctionCall] meta: Optional[Meta] = meta_field() @@ -2770,6 +3841,7 @@ def __str__(self) -> str: @dataclass @_rule_handler("else_if_clause", comments=True) class ElseIfClause: + """The else-if ``ELSIF`` part of an ``IF/ELSIF/ELSE/END_IF`` block.""" if_expression: Expression statements: Optional[StatementList] meta: Optional[Meta] = meta_field() @@ -2787,6 +3859,7 @@ def __str__(self): @dataclass @_rule_handler("else_clause", comments=True) class ElseClause: + """The ``ELSE`` part of an ``IF/ELSIF/ELSE/END_IF`` block.""" statements: Optional[StatementList] meta: Optional[Meta] = meta_field() @@ -2803,6 +3876,7 @@ def __str__(self): @dataclass @_rule_handler("if_statement", comments=True) class IfStatement(Statement): + """The ``IF`` part of an ``IF/ELSIF/ELSE/END_IF`` block.""" if_expression: Expression statements: Optional[StatementList] else_ifs: List[ElseIfClause] @@ -2854,6 +3928,17 @@ def __str__(self): @dataclass @_rule_handler("case_element", comments=True) class CaseElement(Statement): + """ + A single element of a ``CASE`` statement block. + + May contain one or more matches with corresponding statements. Matches + may include subranges, integers, enumerated values, symbolic variables, + bit strings, or boolean values. + + See Also + -------- + :class:`CaseMatch` + """ matches: List[CaseMatch] statements: Optional[StatementList] meta: Optional[Meta] = meta_field() @@ -2882,6 +3967,17 @@ def __str__(self): @dataclass @_rule_handler("case_statement", comments=True) class CaseStatement(Statement): + """ + A switch-like ``CASE`` statement block. + + May contain one or more cases with corresponding statements, and a default + ``ELSE`` clause. + + See Also + -------- + :class:`CaseElement` + :class:`ElseClause` + """ expression: Expression cases: List[CaseElement] else_clause: Optional[ElseClause] @@ -2902,28 +3998,38 @@ def __str__(self) -> str: @dataclass @_rule_handler("no_op_statement", comments=True) class NoOpStatement(Statement): - variable: Variable - meta: Optional[Meta] = meta_field() + """ + A no-operation statement referring to a variable and nothing else. - def __str__(self): - return f"{self.variable};" + Distinguished from an action depending on if the context-sensitive + name matches an action or a variable name. + Note that blark does not handle this for you and may arbitrarily choose + one or the other. -@dataclass -@_rule_handler("action_statement", comments=True) -class ActionStatement(Statement): - # TODO: overlaps with no-op statement? - action: lark.Token + Examples:: + + variable; + """ + variable: Variable meta: Optional[Meta] = meta_field() def __str__(self): - return f"{self.action};" + return f"{self.variable};" @dataclass @_rule_handler("set_statement", comments=True) class SetStatement(Statement): + """ + A "set" statement which conditionally sets a variable to ``TRUE``. + + Examples:: + + bValue S= iValue > 5; + """ variable: SymbolicVariable + op: lark.Token expression: Expression meta: Optional[Meta] = meta_field() @@ -2934,7 +4040,15 @@ def __str__(self): @dataclass @_rule_handler("reference_assignment_statement", comments=True) class ReferenceAssignmentStatement(Statement): + """ + A reference assignment statement. + + Examples:: + + refOne REF= refOtherOne; + """ variable: SymbolicVariable + op: lark.Token expression: Expression meta: Optional[Meta] = meta_field() @@ -2945,7 +4059,15 @@ def __str__(self): @dataclass @_rule_handler("reset_statement") class ResetStatement(Statement): + """ + A "reset" statement which conditionally clears a variable to ``FALSE``. + + Examples:: + + bValue R= iValue <= 5; + """ variable: SymbolicVariable + op: lark.Token expression: Expression meta: Optional[Meta] = meta_field() @@ -2956,6 +4078,7 @@ def __str__(self): @dataclass @_rule_handler("exit_statement", comments=True) class ExitStatement(Statement): + """A statement used to exit a loop, ``EXIT``.""" meta: Optional[Meta] = meta_field() def __str__(self): @@ -2965,6 +4088,7 @@ def __str__(self): @dataclass @_rule_handler("continue_statement", comments=True) class ContinueStatement(Statement): + """A statement used to jump to the top of a loop, ``CONTINUE``.""" meta: Optional[Meta] = meta_field() def __str__(self): @@ -2974,6 +4098,11 @@ def __str__(self): @dataclass @_rule_handler("return_statement", comments=True) class ReturnStatement(Statement): + """ + A statement used to return from a function [block], ``RETURN``. + + No value is allowed to be returned with this statement. + """ meta: Optional[Meta] = meta_field() def __str__(self): @@ -2983,13 +4112,22 @@ def __str__(self): @dataclass @_rule_handler("assignment_statement", comments=True) class AssignmentStatement(Statement): + """ + An assignment statement. + + Examples:: + + iValue := 5; + iValue1 := iValue2 := 6; + """ variables: List[Variable] expression: Expression meta: Optional[Meta] = meta_field() @staticmethod def from_lark(*args) -> AssignmentStatement: - *variables, expression = args + *variables_and_ops, expression = args + variables = variables_and_ops[::2] return AssignmentStatement( variables=list(variables), expression=expression @@ -3003,6 +4141,7 @@ def __str__(self): @dataclass @_rule_handler("while_statement", comments=True) class WhileStatement(Statement): + """A beginning conditional loop statement, ``WHILE``.""" expression: Expression statements: StatementList meta: Optional[Meta] = meta_field() @@ -3022,6 +4161,7 @@ def __str__(self): @dataclass @_rule_handler("repeat_statement", comments=True) class RepeatStatement(Statement): + """An ending conditional loop statement, ``REPEAT``.""" statements: StatementList expression: Expression meta: Optional[Meta] = meta_field() @@ -3041,6 +4181,21 @@ def __str__(self): @dataclass @_rule_handler("for_statement", comments=True) class ForStatement(Statement): + """ + A loop with a control variable and a start, stop, and (optional) step value. + + Examples:: + + FOR iIndex := 0 TO 10 + DO + iValue := iIndex * 2; + END_FOR + + FOR iIndex := (iValue - 5) TO (iValue + 5) BY 2 + DO + arrArray[iIndex] := iIndex * 2; + END_FOR + """ control: SymbolicVariable from_: Expression to: Expression @@ -3068,6 +4223,20 @@ def __str__(self) -> str: comments=True, ) class LabeledStatement(Statement): + """ + A statement marked with a user-defined label. + + This is to support the "goto"-style ``JMP``. + + Examples:: + + label1: A := 1; + + label2: + IF iValue = 1 THEN + A := 3; + END_IF + """ label: lark.Token statement: Optional[Statement] = None meta: Optional[Meta] = meta_field() @@ -3087,6 +4256,13 @@ def __str__(self) -> str: @dataclass @_rule_handler("jmp_statement", comments=True) class JumpStatement(Statement): + """ + This is the "goto"-style ``JMP``, which points at a label. + + Examples:: + + JMP label; + """ label: lark.Token meta: Optional[Meta] = meta_field() @@ -3097,6 +4273,7 @@ def __str__(self) -> str: @dataclass @_rule_handler("statement_list", "case_element_statement_list") class StatementList: + """A list of statements, making up a structured text implementation.""" statements: List[Statement] meta: Optional[Meta] = meta_field() @@ -3132,6 +4309,23 @@ def __str__(self) -> str: @dataclass @_rule_handler("data_type_declaration", comments=True) class DataTypeDeclaration: + """ + A data type declaration, wrapping the other declaration types with + ``TYPE``/``END_TYPE``. + + Access specifiers may be included. + + See Also + -------- + :class:`AccessSpecifier` + :class:`ArrayTypeDeclaration` + :class:`StructureTypeDeclaration` + :class:`StringTypeDeclaration` + :class:`SimpleTypeDeclaration` + :class:`SubrangeTypeDeclaration` + :class:`EnumeratedTypeDeclaration` + :class:`UnionTypeDeclaration` + """ declaration: Optional[TypeDeclarationItem] access: Optional[AccessSpecifier] meta: Optional[Meta] = meta_field() @@ -3179,7 +4373,20 @@ def __str__(self) -> str: @dataclass @_rule_handler("iec_source") class SourceCode: - """Top-level source code item.""" + """ + Top-level source code item. + + May contain zero or more of the following as items: + + * :class:`DataTypeDeclaration` + * :class:`Function` + * :class:`FunctionBlock` + * :class:`Action` + * :class:`Method` + * :class:`Program` + * :class:`Property` + * :class:`GlobalVariableDeclarations` + """ items: List[SourceCodeItem] filename: Optional[pathlib.Path] = None raw_source: Optional[str] = None @@ -3212,6 +4419,11 @@ class ExtendedSourceCode(SourceCode): """ Top-level source code item - extended to include the possibility of standalone implementation details (i.e., statement lists). + + See Also + -------- + :class:`SourceCodeItem` + :class:`StatementList` """ items: List[Union[SourceCodeItem, StatementList]] diff --git a/blark/util.py b/blark/util.py index 8b450a9..b4e6cca 100644 --- a/blark/util.py +++ b/blark/util.py @@ -3,6 +3,7 @@ import codecs import dataclasses import enum +import functools import hashlib import os import pathlib @@ -330,7 +331,6 @@ def find_and_clean_comments( ``"(* abc *)"``. """ lines = text.splitlines() - original_lines = list(lines) multiline_comments = [] in_single_comment = False in_single_quote = False @@ -356,18 +356,14 @@ def fix_line(lineno: int, colno: int) -> str: return "".join(replacement_line) def get_token( - start_line: int, start_col: int, end_line: int, end_col: int + start_pos, + start_line: int, + start_col: int, + end_pos: int, + end_line: int, + end_col: int, ) -> lark.Token: - if start_line != end_line: - block = "\n".join( - ( - original_lines[start_line][start_col:], - *original_lines[start_line + 1: end_line], - original_lines[end_line][: end_col + 1], - ) - ) - else: - block = original_lines[start_line][start_col: end_col + 1] + block = text[start_pos:end_pos + 1] if block.startswith("//"): type_ = "SINGLE_LINE_COMMENT" @@ -376,7 +372,7 @@ def get_token( elif block.startswith("{"): # } type_ = "PRAGMA" else: - raise RuntimeError("Unexpected block: {contents}") + raise RuntimeError(f"Unexpected block: {block!r}") if start_line != end_line: # TODO: move "*)" to separate line @@ -392,8 +388,10 @@ def get_token( token = lark.Token( type_, block, + start_pos=start_pos, line=start_line + 1, end_line=end_line + 1, + end_pos=end_pos, column=start_col + 1, end_column=end_col + 1, ) @@ -404,7 +402,7 @@ def get_token( # token.end_line = line_map.get(end_line + 1, end_line + 1) return token - for lineno, colno, this_ch, next_ch in get_characters(): + for pos, (lineno, colno, this_ch, next_ch) in enumerate(get_characters()): if skip: skip -= 1 continue @@ -416,13 +414,20 @@ def get_token( pair = this_ch + next_ch if not in_single_quote and not in_double_quote: if this_ch == OPEN_PRAGMA and not multiline_comments: - pragma_state.append((lineno, colno)) + pragma_state.append((pos, lineno, colno)) continue if this_ch == CLOSE_PRAGMA and not multiline_comments: - start_line, start_col = pragma_state.pop(-1) + start_pos, start_line, start_col = pragma_state.pop(-1) if len(pragma_state) == 0: comments_and_pragmas.append( - get_token(start_line, start_col, lineno, colno + 1) + get_token( + start_pos, + start_line, + start_col, + pos, + lineno, + colno + 1, + ) ) continue @@ -430,27 +435,41 @@ def get_token( continue if pair == OPEN_COMMENT: - multiline_comments.append((lineno, colno)) + multiline_comments.append((pos, lineno, colno)) skip = 1 if len(multiline_comments) > 1: # Nested multi-line comment lines[lineno] = fix_line(lineno, colno) continue if pair == CLOSE_COMMENT: - start_line, start_col = multiline_comments.pop(-1) + start_pos, start_line, start_col = multiline_comments.pop(-1) if len(multiline_comments) > 0: # Nested multi-line comment lines[lineno] = fix_line(lineno, colno) else: comments_and_pragmas.append( - get_token(start_line, start_col, lineno, colno + 1) + get_token( + start_pos, + start_line, + start_col, + pos + 1, # two character ending + lineno, + colno + 1, # two character ending + ) ) skip = 1 continue if pair == SINGLE_COMMENT: in_single_comment = True comments_and_pragmas.append( - get_token(lineno, colno, lineno, len(lines[lineno])) + get_token( + pos, + lineno, + colno, + pos + (len(lines[lineno]) - colno - 1), + lineno, + len(lines[lineno]), + ) ) continue @@ -622,6 +641,43 @@ def recursively_remove_keys(obj, keys: Set[str]) -> Any: return obj +def simplify_brackets(text: str, brackets: str = "[]") -> str: + """ + Simplify repeated brackets/parentheses in ``text``. + + Parameters + ---------- + text : str + The text to process. + brackets : str, optional + Remove this flavor of brackets - a 2 character string of open and close + brackets. Defaults to ``"[]"``. + """ + open_ch, close_ch = brackets + open_stack: List[int] = [] + start_to_end: Dict[int, int] = {} + to_remove: List[int] = [] + for idx, ch in enumerate(text): + if ch == open_ch: + open_stack.append(idx) + elif ch == close_ch: + if not open_stack: + raise ValueError(f"Unbalanced {brackets} in {text!r}") + open_pos = open_stack.pop(-1) + if start_to_end.get(open_pos + 1, -1) == idx - 1: + to_remove.append(open_pos) + to_remove.append(idx) + start_to_end[open_pos] = idx + + if not to_remove: + return text + + if open_stack: + raise ValueError(f"Unbalanced {brackets} in {text!r}") + + return "".join(ch for idx, ch in enumerate(text) if idx not in to_remove) + + def maybe_add_brackets(text: str, brackets: str = "[]") -> str: """ Add brackets to ``text`` if there are no enclosing brackets. @@ -651,3 +707,54 @@ def maybe_add_brackets(text: str, brackets: str = "[]") -> str: if start_to_end[0] == len(text): return text[1:-1] return text + + +@functools.lru_cache() +def get_grammar_source() -> str: + from . import GRAMMAR_FILENAME + with open(GRAMMAR_FILENAME) as fp: + return fp.read() + + +def get_grammar_for_rule(rule: str) -> str: + """ + Get the lark grammar source for the provided rule. + + Parameters + ---------- + rule : str + The grammar identifier - rule or token name. + """ + # TODO: there may be support for this in lark; consider refactoring + + def split_rule(text: str) -> str: + """ + ``text`` contains the rule and the remainder of ``iec.lark``. + + Split it to just contain the rule, removing the rest. + """ + lines = text.splitlines() + for idx, line in enumerate(lines[1:], 1): + line = line.strip() + if not line.startswith("|"): + return "\n".join(lines[:idx]) + return text + + match = re.search( + rf"^\s*(.*->\s*{rule}$)", + get_grammar_source(), + flags=re.MULTILINE, + ) + if match is not None: + return match.groups()[0] + + match = re.search( + rf"^(\??{rule}(\.\d)?:.*)", + get_grammar_source(), + flags=re.MULTILINE | re.DOTALL, + ) + if match is not None: + text = match.groups()[0] + return split_rule(text) + + raise ValueError(f"Grammar rule not found in source: {rule}") diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..affc98f --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,25 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS ?= +SPHINXBUILD = sphinx-build +SPHINXPROJ = cookiecutterproject_name +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + python update_api_list.py > $(SOURCEDIR)/api.rst + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +clean: + -rm -rf $(BUILDDIR) + -rm -rf $(SOURCEDIR)/api diff --git a/docs/_templates/autosummary/base.rst b/docs/_templates/autosummary/base.rst new file mode 100644 index 0000000..b7556eb --- /dev/null +++ b/docs/_templates/autosummary/base.rst @@ -0,0 +1,5 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst new file mode 100644 index 0000000..de0ba07 --- /dev/null +++ b/docs/_templates/autosummary/class.rst @@ -0,0 +1,50 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + + {% set grammar = get_grammar_for_class(objname) %} + {% if grammar %} + {% block grammar %} + .. rubric:: Lark grammar + + This class is used by the following grammar rules: + + {% for name, source in grammar.items() %} + ``{{ name }}`` + + .. code:: + + {{ source | indent(" ") }} + + {% endfor %} + {% endblock %} + {% endif %} + + {% block methods %} + {% if methods %} + .. rubric:: {{ _('Methods') }} + + .. autosummary:: + + {% for item in methods %} + {%- if item not in inherited_members %} + ~{{ name }}.{{ item }} + {%- endif %} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + + {%- endif %} + {% endblock %} diff --git a/docs/_templates/autosummary/module.rst b/docs/_templates/autosummary/module.rst new file mode 100644 index 0000000..93ccbb1 --- /dev/null +++ b/docs/_templates/autosummary/module.rst @@ -0,0 +1,64 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Module Attributes') }} + + .. autosummary:: + :toctree: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + :toctree: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + :toctree: + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + :toctree: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. rubric:: Modules + +.. autosummary:: + :toctree: + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..fad53da --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build +set SPHINXPROJ=cookiecutterproject_name + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/source/_static/.gitkeep b/docs/source/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..989b4f0 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import pathlib +import sys +from datetime import datetime +from typing import Dict + +import sphinx_rtd_theme # noqa: F401 + +docs_source_path = pathlib.Path(__file__).parent.resolve() +docs_path = docs_source_path.parent +module_path = docs_path.parent +sys.path.insert(0, str(module_path)) + +# -- Project information ----------------------------------------------------- + +project = "blark" +author = "Ken Lauer" + +year = datetime.now().year +copyright = f"{year}, {author}" + +# The short X.Y version +version = "" +# The full version, including alpha/beta/rc tags +release = "" + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.inheritance_diagram", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.ifconfig", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + # jquery removed in sphinx 6.0 and used in docs_versions_menu. + # See: https://www.sphinx-doc.org/en/master/changes.html + "sphinxcontrib.jquery", + "numpydoc", + "docs_versions_menu", + "sphinx_rtd_theme", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = [str(docs_path / "_templates")] + +autosummary_generate = True + +autodoc_default_options = { + "show-inheritance": True, + "members": True, + "member-order": "bysource", + "special-members": "__init__", + "undoc-members": True, + "exclude-members": "__weakref__", +} +autoclass_content = "both" + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# +default_role = 'any' + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# html_css_files = [] +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = "blark_namedoc" + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + master_doc, + "blark.tex", + "blark Documentation", + "Ken Lauer", + "manual", + ), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ( + master_doc, + "blark", + "blark Documentation", + [author], + 1, + ) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "blark", + "blark Documentation", + author, + "blark", + "TwinCAT Structured Text Parsing Tools", + "Miscellaneous", + ), +] + +# -- Extension configuration ------------------------------------------------- + +# Intersphinx +intersphinx_mapping = { + # 'python': ('https://docs.python.org/3', None), +} + + +# Inheritance diagram settings +inheritance_graph_attrs = dict( + rankdir="TB", + size='""', +) + +inheritance_alias = { +} + +numpydoc_show_class_members = False + + +def get_grammar_for_class(clsname: str) -> Dict[str, str]: + """ + Given a class name, get blark's ``iec.lark`` associated grammar + definition(s). + """ + import blark + try: + cls = getattr(blark.transform, clsname) + except AttributeError: + return {} + + return blark.transform.get_grammar_for_class(cls) + + +autosummary_context = { + "get_grammar_for_class": get_grammar_for_class, + "generated_toctree": "api", +} diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..abce19a --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,32 @@ +blark +===== + +.. toctree:: + :maxdepth: 2 + :caption: User documentation + + introduction.rst + sphinx.rst + + +.. toctree:: + :maxdepth: 2 + :caption: Developer documentation + + api.rst + +.. toctree:: + :maxdepth: 1 + :caption: Links + :hidden: + + GitHub Repository + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst new file mode 100644 index 0000000..a649f98 --- /dev/null +++ b/docs/source/introduction.rst @@ -0,0 +1,286 @@ +Introduction +############ + +The Grammar +----------- + +The `grammar `__ uses Lark’s Earley parser algorithm. + +The grammar itself is not perfect. It may not reliably parse your source +code or produce useful Python instances just yet. + +See `issues `__ for further +details. + +As a fun side project, blark isn’t at the top of my priority list. For +an idea of where the project is going, see the issues list. + +Requirements +------------ + +- `lark `__ (for grammar-based + parsing) +- `lxml `__ (for parsing TwinCAT + projects) + +Capabilities +------------ + +- TwinCAT source code file parsing (``*.TcPOU`` and others) +- TwinCAT project and solution loading +- ``lark.Tree`` generation of any supported source code +- Python dataclasses of supported source code, with introspection and + code refactoring + +Works-in-progress +~~~~~~~~~~~~~~~~~ + +- Sphinx API documentation generation (a new Sphinx domain) +- Code reformatting +- “Dependency store” - recursively parse and inspect project + dependencies +- Summary generation - a layer on top of dataclasses to summarize + source code details +- Rewriting source code directly in TwinCAT source code files + +Installation +------------ + +Installation is quick with Pip. + +.. code:: bash + + pip install --upgrade blark + +Quickstart (pip / virtualenv with venv) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Set up an environment using venv: + +.. code:: bash + + $ python -m venv blark_venv + $ source blark_venv/bin/activate + +2. Install the library with pip: + +.. code:: bash + + $ python -m pip install blark + +Quickstart (Conda) +~~~~~~~~~~~~~~~~~~ + +1. Set up an environment using conda: + +.. code:: bash + + $ conda create -n blark-env -c conda-forge python=3.10 pip blark + $ conda activate blark-env + +2. Install the library from conda: + +.. code:: bash + + $ conda install blark + +Development install +~~~~~~~~~~~~~~~~~~~ + +If you run into issues or wish to run an unreleased version of blark, +you may install directly from this repository like so: + +.. code:: bash + + $ python -m pip install git+https://github.com/klauer/blark + +Sample runs +----------- + +Run the parser or experimental formatter utility. Current supported file +types include those from TwinCAT3 projects ( ``.tsproj``, ``.sln``, +``.TcPOU``, ``.TcGVL``) and plain-text ``.st`` files. + +.. code:: bash + + $ blark parse --print-tree blark/tests/POUs/F_SetStateParams.TcPOU + function_declaration + None + F_SetStateParams + indirect_simple_specification + None + simple_specification BOOL + input_declarations + None + var1_init_decl + var1_list + ... (clipped) ... + +To interact with the Python dataclasses directly, make sure IPython is +installed first and then try: + +:: + + $ blark parse --interactive blark/tests/POUs/F_SetStateParams.TcPOU + # Assuming IPython is installed, the following prompt will come up: + + In [1]: results[0].identifier + Out[1]: 'F_SetStateParams/declaration' + + In [2]: results[1].identifier + Out[2]: 'F_SetStateParams/implementation' + +Dump out a parsed and reformatted set of source code: + +.. code:: bash + + $ blark format blark/tests/source/array_of_objects.st + {attribute 'hide'} + METHOD prv_Detection : BOOL + VAR_IN_OUT + currentChannel : ARRAY [APhase..CPhase] OF class_baseVector(SIZEOF(vector_t), 0); + END_VAR + END_METHOD + +blark supports rewriting TwinCAT source code files directly as well: + +.. code:: bash + + $ blark format blark/tests/POUs/F_SetStateParams.TcPOU + + + + `__ (GitHub fork +`here `__) and `A Syntactic +Specification for the Programming Languages of theIEC 61131-3 +Standard `__ +by Flor Narciso et al. Many aspects of the grammar have been added to, +modified, and in cases entirely rewritten to better support lark +grammars and transformers. + +Special thanks to the blark contributors: + +- @engineerjoe440 + +Related, Similar, or Alternative Projects +----------------------------------------- + +There are a number of similar, or related projects that are available. + +- `“MATIEC” `__ - another IEC + 61131-3 Structured Text parser which supports IEC 61131-3 second + edition, without classes, namespaces and other fancy features. An + updated version is also `available on + Github `__ +- `OpenPLC Runtime Version + 3 `__ - As stated by the + project: > OpenPLC is an open-source Programmable Logic Controller + that is based on easy to use software. Our focus is to provide a low + cost industrial solution for automation and research. OpenPLC has + been used in many research papers as a framework for industrial cyber + security research, given that it is the only controller to provide + the entire source code. +- `RuSTy `__ + `documentation `__ - + Structured text compiler written in Rust. As stated by the project: > + RuSTy is a structured text (ST) compiler written in Rust. RuSTy + utilizes the LLVM framework to compile eventually to native code. +- `IEC Checker `__ - Static + analysis tool for IEC 61131-3 logic. As described by the maintainer: + > iec-checker has the ability to parse ST source code and dump AST + and CFG to JSON format, so you can process it with your language of + choice. +- `TcBlack `__ - Python black-like + code formatter for TwinCAT code. diff --git a/docs/source/sphinx.rst b/docs/source/sphinx.rst new file mode 100644 index 0000000..c772ead --- /dev/null +++ b/docs/source/sphinx.rst @@ -0,0 +1,2 @@ +Sphinx API Docs +############### diff --git a/docs/update_api_list.py b/docs/update_api_list.py new file mode 100644 index 0000000..872d9c8 --- /dev/null +++ b/docs/update_api_list.py @@ -0,0 +1,150 @@ +""" +Tool that generates the source for ``source/api.rst``. +""" +from __future__ import annotations + +import inspect +import pathlib +import sys +from types import ModuleType +from typing import Callable, Optional + +docs_path = pathlib.Path(__file__).parent.resolve() +module_path = docs_path.parent +sys.path.insert(0, str(module_path)) + +import blark # noqa: E402 +import blark.apischema_compat # noqa: E402 +import blark.config # noqa: E402 +import blark.dependency_store # noqa: E402 +import blark.format # noqa: E402 +import blark.html # noqa: E402 +import blark.input # noqa: E402 +import blark.main # noqa: E402 +import blark.output # noqa: E402 +import blark.parse # noqa: E402 +import blark.plain # noqa: E402 +import blark.solution # noqa: E402 +import blark.sphinxdomain # noqa: E402 +import blark.summary # noqa: E402 +import blark.transform # noqa: E402 +import blark.typing # noqa: E402 +import blark.util # noqa: E402 + + +def find_all_classes( + modules, + base_classes: tuple[type, ...], + skip: Optional[list[str]] = None, +) -> list[type]: + """Find all classes in the module and return them as a list.""" + skip = skip or [] + + def should_include(obj): + return ( + inspect.isclass(obj) and + (not base_classes or issubclass(obj, base_classes)) and + obj.__name__ not in skip + ) + + def sort_key(cls): + return (cls.__module__, cls.__name__) + + classes = [ + obj + for module in modules + for _, obj in inspect.getmembers(module, predicate=should_include) + ] + + return list(sorted(set(classes), key=sort_key)) + + +def find_callables(modules: list[ModuleType]) -> list[Callable]: + """Find all callables in the module and return them as a list.""" + def should_include(obj): + try: + name = obj.__name__ + module = obj.__module__ + except AttributeError: + return False + + if not any(module.startswith(mod.__name__) for mod in modules): + return False + + return ( + callable(obj) and + not inspect.isclass(obj) and + not name.startswith("_") + ) + + def sort_key(obj): + return (obj.__module__, obj.__name__) + + callables = [ + obj + for module in modules + for _, obj in inspect.getmembers(module, predicate=should_include) + ] + + return list(sorted(set(callables), key=sort_key)) + + +def create_api_list(modules: list[ModuleType]) -> list[str]: + """Create the API list with all classes and functions.""" + output = [ + "API", + "###", + "", + ] + + for module in modules: + classes = find_all_classes([module], base_classes=()) + callables = find_callables([module]) + module_name = module.__name__ + underline = "-" * len(module_name) + output.append(module_name) + output.append(underline) + output.append("") + objects = [ + obj + for obj in list(classes) + list(callables) + if obj.__module__ == module_name and hasattr(module, obj.__name__) + ] + + if objects: + output.append(".. autosummary::") + output.append(" :toctree: api") + output.append("") + + for obj in sorted(objects, key=lambda obj: obj.__name__): + output.append(f" {obj.__module__}.{obj.__name__}") + + output.append("") + + while output[-1] == "": + output.pop(-1) + return output + + +if __name__ == "__main__": + output = create_api_list( + [ + blark.apischema_compat, + blark.config, + blark.dependency_store, + blark.format, + blark.html, + blark.input, + blark.main, + blark.output, + blark.parse, + blark.plain, + blark.solution, + blark.sphinxdomain, + blark.summary, + blark.transform, + blark.typing, + blark.util, + ] + ) + print("\n".join(output))