Source code for pyfcstm.dsl.node

"""
State Machine DSL Abstract Syntax Tree (AST) Nodes.

This module defines the Abstract Syntax Tree (AST) building blocks used by the
State Machine Domain Specific Language (DSL). The classes provide a structured
representation of DSL constructs such as states, transitions, expressions,
and executable actions. Instances of these classes are typically produced by
a parser and can be serialized back to DSL syntax via ``str()``.

The main public components include:

* :class:`ASTNode` - Root base class for all AST nodes.
* :class:`Expr` and :class:`Literal` families - Expression nodes and literals.
* :class:`StateDefinition` - State declarations with nested structure.
* :class:`TransitionDefinition` and :class:`ForceTransitionDefinition` - State transitions.
* Import-related nodes such as :class:`ImportStatement`,
  :class:`ImportDefMapping`, and :class:`ImportEventMapping`.
* :class:`StateMachineDSLProgram` - Root program container.
* Statement and action blocks such as :class:`OperationAssignment`,
  :class:`EnterStatement`, :class:`ExitStatement`, and :class:`DuringStatement`.

.. note::
   The AST classes are data-only and focus on structure. There is no evaluation
   or execution logic in this module.

Example::

    >>> from pyfcstm.dsl.node import (
    ...     StateDefinition, TransitionDefinition, ChainID,
    ...     StateMachineDSLProgram, DefAssignment, Integer
    ... )
    >>> def_var = DefAssignment("counter", "int", Integer("0"))
    >>> trans = TransitionDefinition("idle", "active", ChainID(["idle", "start"]), None, [])
    >>> root = StateDefinition("idle", transitions=[trans])
    >>> program = StateMachineDSLProgram([def_var], root)
    >>> print(program)
    def int counter = 0;
    state idle {
        idle -> active :: start;
    }

"""

import io
import json
import math
import os
from abc import ABC
from dataclasses import dataclass, field
from textwrap import indent
from typing import List, Union, Optional, Any

from hbutils.design import SingletonMark

from ..utils.validate import Span

__all__ = [
    "ASTNode",
    "Identifier",
    "ChainID",
    "Expr",
    "Literal",
    "Boolean",
    "Integer",
    "HexInt",
    "Float",
    "Constant",
    "Name",
    "Paren",
    "UnaryOp",
    "BinaryOp",
    "ConditionalOp",
    "UFunc",
    "Statement",
    "ConstantDefinition",
    "InitialAssignment",
    "DefAssignment",
    "ImportMappingStatement",
    "ImportDefSelector",
    "ImportDefExactSelector",
    "ImportDefSetSelector",
    "ImportDefPatternSelector",
    "ImportDefFallbackSelector",
    "ImportDefTargetTemplate",
    "ImportDefMapping",
    "ImportEventMapping",
    "ImportStatement",
    "OperationalDeprecatedAssignment",
    "Preamble",
    "Operation",
    "Condition",
    "TransitionDefinition",
    "ForceTransitionDefinition",
    "StateDefinition",
    "OperationalStatement",
    "OperationAssignment",
    "OperationIfBranch",
    "OperationIf",
    "EventDefinition",
    "StateMachineDSLProgram",
    "INIT_STATE",
    "EXIT_STATE",
    "ALL",
    "EnterStatement",
    "EnterOperations",
    "EnterAbstractFunction",
    "EnterRefFunction",
    "ExitStatement",
    "ExitOperations",
    "ExitAbstractFunction",
    "ExitRefFunction",
    "DuringStatement",
    "DuringOperations",
    "DuringAbstractFunction",
    "DuringRefFunction",
    "DuringAspectStatement",
    "DuringAspectOperations",
    "DuringAspectAbstractFunction",
    "DuringAspectRefFunction",
]


[docs] @dataclass class ASTNode(ABC): """ Abstract base class for all AST nodes in the state machine DSL. This class provides a common ancestor for all nodes in the Abstract Syntax Tree, making it convenient to type-check and traverse mixed node collections. :rtype: ASTNode """ pass
[docs] @dataclass class Identifier(ASTNode): """ Abstract base class for identifiers in the state machine DSL. Identifiers are used to reference variables, states, and other named elements in the state machine definition. :rtype: Identifier """ pass
[docs] @dataclass class ChainID(Identifier): """ Represents a chained identifier (e.g., ``a.b.c``) in the state machine DSL. :param path: List of string components that make up the chained identifier :type path: List[str] :param is_absolute: Whether the identifier is absolute (starts with ``/``) :type is_absolute: bool :rtype: ChainID Example:: >>> chain_id = ChainID(['state1', 'event']) >>> str(chain_id) 'state1.event' >>> abs_id = ChainID(['root', 'event'], is_absolute=True) >>> str(abs_id) '/root.event' """ path: List[str] is_absolute: bool = False
[docs] def __str__(self) -> str: """ Convert the :class:`ChainID` to its string representation. :return: String representation of the chained identifier :rtype: str """ pth = ".".join(self.path) if self.is_absolute: pth = f"/{pth}" return pth
[docs] @dataclass class Expr(ASTNode): """ Abstract base class for expressions in the state machine DSL. Expressions represent computations that produce values, which can be used in conditions, assignments, and other contexts within the state machine. :rtype: Expr """ pass
[docs] @dataclass class Literal(Expr): """ Base class for literal values in expressions. Literal values are constants directly expressed in the code, such as numbers, booleans, or predefined constants. :param raw: The raw string representation of the literal :type raw: str :rtype: Literal """ raw: str @property def value(self) -> Any: """ Get the actual value of the literal. :return: The evaluated value of the literal :rtype: Any """ return self._value() def _value(self) -> Any: """ Internal method to evaluate the literal's value. :return: The evaluated value of the literal :rtype: Any :raises NotImplementedError: This method must be implemented by subclasses """ raise NotImplementedError # pragma: no cover
[docs] def __str__(self) -> str: """ Convert the literal to its string representation. :return: String representation of the literal's value :rtype: str """ return str(self._value())
[docs] @dataclass class Integer(Literal): """ Represents an integer literal in the state machine DSL. :param raw: The raw string representation of the integer :type raw: str :rtype: Integer Example:: >>> int_val = Integer("42") >>> int_val.value 42 """ def _value(self) -> int: """ Convert the raw string to an integer value. :return: The integer value :rtype: int """ return int(self.raw)
[docs] @dataclass class HexInt(Literal): """ Represents a hexadecimal integer literal in the state machine DSL. :param raw: The raw string representation of the hexadecimal integer (e.g., ``"0xFF"``) :type raw: str :rtype: HexInt Example:: >>> hex_val = HexInt("0xFF") >>> hex_val.value 255 """ def _value(self) -> int: """ Convert the raw hexadecimal string to an integer value. :return: The integer value :rtype: int """ return int(self.raw, 16)
[docs] def __str__(self) -> str: """ Convert the hexadecimal integer to its string representation. :return: Lowercase string representation of the hexadecimal value :rtype: str """ return self.raw.lower()
[docs] @dataclass class Float(Literal): """ Represents a floating-point literal in the state machine DSL. :param raw: The raw string representation of the float :type raw: str :rtype: Float Example:: >>> float_val = Float("3.14") >>> float_val.value 3.14 """ def _value(self) -> float: """ Convert the raw string to a float value. :return: The float value :rtype: float """ return float(self.raw)
[docs] def __str__(self) -> str: """ Convert the float to its string representation. :return: String representation of the float :rtype: str """ return self.raw
[docs] @dataclass class Boolean(Literal): """ Represents a boolean literal in the state machine DSL. :param raw: The raw string representation of the boolean (``"true"`` or ``"false"``) :type raw: str :rtype: Boolean Example:: >>> bool_val = Boolean("true") >>> bool_val.value True """
[docs] def __post_init__(self) -> None: """ Normalize the raw value to lowercase after initialization. """ self.raw = self.raw.lower()
def _value(self) -> bool: """ Convert the raw string to a boolean value. :return: The boolean value :rtype: bool """ return json.loads(self.raw)
[docs] @dataclass class Constant(Literal): """ Represents a named mathematical constant in the state machine DSL. :param raw: The name of the constant (e.g., ``"pi"``, ``"E"``) :type raw: str :rtype: Constant Example:: >>> pi_const = Constant("pi") >>> pi_const.value 3.141592653589793 """ __KNOWN_CONSTANTS__ = { "E": math.e, "pi": math.pi, "tau": math.tau, } def _value(self) -> float: """ Get the value of the named constant. :return: The constant's value :rtype: float """ return self.__KNOWN_CONSTANTS__[self.raw]
[docs] def __str__(self) -> str: """ Convert the constant to its string representation. :return: The name of the constant :rtype: str """ return f"{self.raw}"
[docs] @dataclass class Name(Expr): """ Represents a named reference in the state machine DSL. Names are used to reference variables, states, or other named elements. :param name: The identifier name :type name: str :rtype: Name Example:: >>> var_name = Name("counter") >>> str(var_name) 'counter' """ name: str
[docs] def __str__(self) -> str: """ Convert the name to its string representation. :return: The name as a string :rtype: str """ return self.name
[docs] @dataclass class Paren(Expr): """ Represents a parenthesized expression in the state machine DSL. Parentheses are used to control the order of operations in expressions. :param expr: The expression within the parentheses :type expr: Expr :rtype: Paren Example:: >>> inner_expr = BinaryOp(Name("a"), "+", Name("b")) >>> paren_expr = Paren(inner_expr) >>> str(paren_expr) '(a + b)' """ expr: Expr
[docs] def __str__(self) -> str: """ Convert the parenthesized expression to its string representation. :return: The expression surrounded by parentheses :rtype: str """ return f"({self.expr})"
[docs] @dataclass class UnaryOp(Expr): """ Represents a unary operation in the state machine DSL. Unary operations apply a single operator to an expression. :param op: The unary operator (e.g., ``"!"``, ``"not"``, ``"-"``) :type op: str :param expr: The expression to which the operator is applied :type expr: Expr :rtype: UnaryOp Example:: >>> not_expr = UnaryOp("not", Name("condition")) >>> str(not_expr) '!condition' """ __aliases__ = { "not": "!", } op: str expr: Expr
[docs] def __post_init__(self) -> None: """ Replace any operator aliases with their canonical form. """ self.op = self.__aliases__.get(self.op, self.op)
[docs] def __str__(self) -> str: """ Convert the unary operation to its string representation. :return: String representation of the unary operation :rtype: str """ return f"{self.op}{self.expr}"
[docs] @dataclass class BinaryOp(Expr): """ Represents a binary operation in the state machine DSL. Binary operations apply an operator to two expressions. :param expr1: The left-hand expression :type expr1: Expr :param op: The binary operator (e.g., ``"+"``, ``"-"``, ``"and"``, ``"or"``) :type op: str :param expr2: The right-hand expression :type expr2: Expr :rtype: BinaryOp Example:: >>> add_expr = BinaryOp(Name("a"), "+", Name("b")) >>> str(add_expr) 'a + b' >>> and_expr = BinaryOp(Name("x"), "and", Name("y")) >>> str(and_expr) 'x && y' """ __aliases__ = { "and": "&&", "or": "||", } expr1: Expr op: str expr2: Expr
[docs] def __post_init__(self) -> None: """ Replace any operator aliases with their canonical form. """ self.op = self.__aliases__.get(self.op, self.op)
[docs] def __str__(self) -> str: """ Convert the binary operation to its string representation. :return: String representation of the binary operation :rtype: str """ return f"{self.expr1} {self.op} {self.expr2}"
[docs] @dataclass class ConditionalOp(Expr): """ Represents a conditional (ternary) operation in the state machine DSL. The conditional operation evaluates a condition and returns one of two expressions based on whether the condition is true or false. :param cond: The condition expression :type cond: Expr :param value_true: The expression to evaluate if the condition is true :type value_true: Expr :param value_false: The expression to evaluate if the condition is false :type value_false: Expr :rtype: ConditionalOp Example:: >>> cond_op = ConditionalOp(Name("x"), Integer("1"), Integer("0")) >>> str(cond_op) '(x) ? 1 : 0' """ cond: Expr value_true: Expr value_false: Expr
[docs] def __str__(self) -> str: """ Convert the conditional operation to its string representation. :return: String representation of the conditional operation :rtype: str """ return f"({self.cond}) ? {self.value_true} : {self.value_false}"
[docs] @dataclass class UFunc(Expr): """ Represents a unary function call in the state machine DSL. Unary functions apply a named function to a single expression. :param func: The function name :type func: str :param expr: The expression to which the function is applied :type expr: Expr :rtype: UFunc Example:: >>> func_call = UFunc("abs", Name("x")) >>> str(func_call) 'abs(x)' """ func: str expr: Expr
[docs] def __str__(self) -> str: """ Convert the function call to its string representation. :return: String representation of the function call :rtype: str """ return f"{self.func}({self.expr})"
[docs] @dataclass class Statement(ASTNode): """ Abstract base class for statements in the state machine DSL. Statements represent actions or declarations in the state machine definition. :rtype: Statement """ pass
[docs] @dataclass class OperationalStatement(Statement): """ Abstract base class for statements inside operation blocks. Operation statements are the executable statements that may appear inside ``effect { ... }``, ``enter { ... }``, ``during { ... }``, and ``exit { ... }`` blocks. :rtype: OperationalStatement """ pass
def _render_operational_statement_block( statements: List["OperationalStatement"], ) -> str: """ Render an operation statement list as an indented block body. :param statements: Statements to render. :type statements: List[OperationalStatement] :return: Rendered block body without surrounding braces. :rtype: str """ with io.StringIO() as sf: for statement in statements: print(indent(str(statement), prefix=" "), file=sf) return sf.getvalue()
[docs] @dataclass class ConstantDefinition(Statement): """ Represents a constant definition statement in the state machine DSL. :param name: The name of the constant :type name: str :param expr: The expression defining the constant's value :type expr: Expr :rtype: ConstantDefinition Example:: >>> const_def = ConstantDefinition("MAX_COUNT", Integer("100")) >>> str(const_def) 'MAX_COUNT = 100;' """ name: str expr: Expr
[docs] def __str__(self) -> str: """ Convert the constant definition to its string representation. :return: String representation of the constant definition :rtype: str """ return f"{self.name} = {self.expr};"
[docs] @dataclass class InitialAssignment(Statement): """ Represents an initial assignment statement in the state machine DSL. Initial assignments are used to set initial values for variables. :param name: The name of the variable :type name: str :param expr: The expression defining the variable's initial value :type expr: Expr :rtype: InitialAssignment Example:: >>> init_assign = InitialAssignment("counter", Integer("0")) >>> str(init_assign) 'counter := 0;' """ name: str expr: Expr
[docs] def __str__(self) -> str: """ Convert the initial assignment to its string representation. :return: String representation of the initial assignment :rtype: str """ return f"{self.name} := {self.expr};"
[docs] @dataclass class DefAssignment(Statement): """ Represents a definition assignment statement in the state machine DSL. Definition assignments are used to declare and initialize typed variables. :param name: The name of the variable :type name: str :param type: The type of the variable :type type: str :param expr: The expression defining the variable's value :type expr: Expr :rtype: DefAssignment Example:: >>> def_assign = DefAssignment("counter", "int", Integer("0")) >>> str(def_assign) 'def int counter = 0;' """ name: str type: str expr: Expr _span: Optional[Span] = field(default=None, repr=False, compare=False)
[docs] def __str__(self) -> str: """ Convert the definition assignment to its string representation. :return: String representation of the definition assignment :rtype: str """ return f"def {self.type} {self.name} = {self.expr};"
[docs] @dataclass class OperationalDeprecatedAssignment(Statement): """ Represents a deprecated form of operational assignment in the state machine DSL. :param name: The name of the variable :type name: str :param expr: The expression defining the variable's value :type expr: Expr :rtype: OperationalDeprecatedAssignment Example:: >>> op_assign = OperationalDeprecatedAssignment( ... "counter", BinaryOp(Name("counter"), "+", Integer("1")) ... ) >>> str(op_assign) 'counter := counter + 1;' """ name: str expr: Expr
[docs] def __str__(self) -> str: """ Convert the operational deprecated assignment to its string representation. :return: String representation of the operational deprecated assignment :rtype: str """ return f"{self.name} := {self.expr};"
[docs] @dataclass class Condition(ASTNode): """ Represents a condition in the state machine DSL. Conditions are used in transitions and other contexts to determine when actions should occur. :param expr: The expression defining the condition :type expr: Expr :rtype: Condition Example:: >>> cond = Condition(BinaryOp(Name("counter"), ">=", Integer("10"))) >>> str(cond) 'counter >= 10' """ expr: Expr
[docs] def __str__(self) -> str: """ Convert the condition to its string representation. :return: String representation of the condition :rtype: str """ return f"{self.expr}"
[docs] @dataclass class Preamble(ASTNode): """ Represents a preamble section in the state machine DSL. The preamble contains constant definitions and initial assignments that set up the state machine's environment. :param stats: List of statements in the preamble :type stats: List[Union[ConstantDefinition, InitialAssignment]] :rtype: Preamble Example:: >>> const_def = ConstantDefinition("MAX", Integer("100")) >>> init_assign = InitialAssignment("counter", Integer("0")) >>> preamble = Preamble([const_def, init_assign]) >>> print(str(preamble)) MAX = 100; counter := 0; """ stats: List[Union[ConstantDefinition, InitialAssignment]]
[docs] def __str__(self) -> str: """ Convert the preamble to its string representation. :return: String representation of the preamble :rtype: str """ return os.linesep.join(map(str, self.stats))
[docs] @dataclass class Operation(ASTNode): """ Represents an operation block in the state machine DSL. Operations are sequences of assignments that modify the state machine's variables. :param stats: List of operational assignments :type stats: List[OperationalDeprecatedAssignment] :rtype: Operation Example:: >>> op1 = OperationalDeprecatedAssignment( ... "counter", BinaryOp(Name("counter"), "+", Integer("1")) ... ) >>> op2 = OperationalDeprecatedAssignment("flag", Boolean("true")) >>> operation = Operation([op1, op2]) >>> print(str(operation)) counter := counter + 1; flag := true; """ stats: List[OperationalDeprecatedAssignment]
[docs] def __str__(self) -> str: """ Convert the operation to its string representation. :return: String representation of the operation :rtype: str """ return os.linesep.join(map(str, self.stats))
class _StateSingletonMark(SingletonMark): """ A singleton marker class for special states in the state machine DSL. :param mark: The marker name :type mark: str :rtype: _StateSingletonMark """ def __repr__(self) -> str: """ Convert the singleton mark to its string representation. :return: The marker name :rtype: str """ return self.mark INIT_STATE = _StateSingletonMark("INIT_STATE") """ Special singleton marker representing the initial state in a state machine. This is used to define transitions from the initial pseudo-state. """ EXIT_STATE = _StateSingletonMark("EXIT_STATE") """ Special singleton marker representing the exit state in a state machine. This is used to define transitions to the final pseudo-state. """ ALL = _StateSingletonMark("ALL") """ Special singleton marker representing all states in a state machine. This is used to define transitions or actions that apply to all states. """
[docs] @dataclass class ImportMappingStatement(ASTNode): """ Abstract base class for mapping statements inside an import block. Import mapping statements configure how imported variables and events should be exposed or remapped when the imported module is assembled into the host state tree. :rtype: ImportMappingStatement """ pass
[docs] @dataclass class ImportDefSelector(ASTNode): """ Abstract base class for ``def`` mapping source selectors in import blocks. Source selectors determine which imported variable names a ``def`` mapping rule applies to. :rtype: ImportDefSelector """ pass
[docs] @dataclass class ImportDefExactSelector(ImportDefSelector): """ Represents an exact source variable selector in an import ``def`` mapping. :param name: Exact variable name to match :type name: str :rtype: ImportDefExactSelector """ name: str
[docs] def __str__(self) -> str: """ Convert the selector to its DSL representation. :return: String representation of the selector :rtype: str """ return self.name
[docs] @dataclass class ImportDefSetSelector(ImportDefSelector): """ Represents a set-based source selector in an import ``def`` mapping. :param names: Exact variable names listed in the selector set :type names: List[str] :rtype: ImportDefSetSelector """ names: List[str]
[docs] def __str__(self) -> str: """ Convert the selector to its DSL representation. :return: String representation of the selector :rtype: str """ return "{%s}" % ", ".join(self.names)
[docs] @dataclass class ImportDefPatternSelector(ImportDefSelector): """ Represents a wildcard-based source selector in an import ``def`` mapping. The selector is intentionally preserved as raw DSL text rather than being decomposed into finer-grained AST nodes. This keeps the DSL layer permissive enough for patterns whose literal segments would not fit the plain ``ID`` token constraints, such as suffixes that start with digits. :param pattern: Raw selector pattern text :type pattern: str :rtype: ImportDefPatternSelector """ pattern: str
[docs] def __str__(self) -> str: """ Convert the selector to its DSL representation. :return: String representation of the selector :rtype: str """ return self.pattern
[docs] @dataclass class ImportDefFallbackSelector(ImportDefSelector): """ Represents the fallback ``*`` selector in an import ``def`` mapping. :rtype: ImportDefFallbackSelector """
[docs] def __str__(self) -> str: """ Convert the selector to its DSL representation. :return: Wildcard text :rtype: str """ return "*"
[docs] @dataclass class ImportDefTargetTemplate(ASTNode): """ Represents the right-hand target template of an import ``def`` mapping. The template is stored as raw DSL text at the AST layer. Placeholder and wildcard semantics are validated and interpreted in later assembly phases. :param template: Raw template text :type template: str :rtype: ImportDefTargetTemplate """ template: str
[docs] def __str__(self) -> str: """ Convert the template to its DSL representation. :return: String representation of the target template :rtype: str """ return self.template
[docs] @dataclass class ImportDefMapping(ImportMappingStatement): """ Represents a variable mapping rule inside an import block. :param selector: Source selector of the mapping rule :type selector: ImportDefSelector :param target_template: Target template of the mapping rule :type target_template: ImportDefTargetTemplate :rtype: ImportDefMapping """ selector: ImportDefSelector target_template: ImportDefTargetTemplate
[docs] def __str__(self) -> str: """ Convert the mapping rule to its DSL representation. :return: String representation of the mapping rule :rtype: str """ return f"def {self.selector} -> {self.target_template};"
[docs] @dataclass class ImportEventMapping(ImportMappingStatement): """ Represents an event mapping rule inside an import block. :param source_event: Source event path inside the imported module :type source_event: ChainID :param target_event: Target event path in the host state tree :type target_event: ChainID :param extra_name: Optional display-name override for the target event :type extra_name: Optional[str] :rtype: ImportEventMapping """ source_event: ChainID target_event: ChainID extra_name: Optional[str] = None
[docs] def __str__(self) -> str: """ Convert the event mapping to its DSL representation. :return: String representation of the event mapping :rtype: str """ with io.StringIO() as sf: print(f"event {self.source_event} -> {self.target_event}", file=sf, end="") if self.extra_name is not None: print(f" named {self.extra_name!r}", file=sf, end="") print(";", file=sf, end="") return sf.getvalue()
[docs] @dataclass class ImportStatement(ASTNode): """ Represents a single import statement inside a composite state. :param source_path: Import source path string from the DSL :type source_path: str :param alias: Local alias name of the imported root state :type alias: str :param extra_name: Optional display-name override for the imported state :type extra_name: Optional[str] :param mappings: Mapping rules declared inside the import block :type mappings: List[ImportMappingStatement] :rtype: ImportStatement """ source_path: str alias: str extra_name: Optional[str] = None mappings: List[ImportMappingStatement] = None
[docs] def __post_init__(self) -> None: """ Initialize default empty lists for optional parameters. """ self.mappings = self.mappings or []
[docs] def __str__(self) -> str: """ Convert the import statement to its DSL representation. :return: String representation of the import statement :rtype: str """ with io.StringIO() as sf: print(f"import {self.source_path!r} as {self.alias}", file=sf, end="") if self.extra_name is not None: print(f" named {self.extra_name!r}", file=sf, end="") if self.mappings: print(" {", file=sf) for mapping in self.mappings: print(indent(str(mapping), prefix=" "), file=sf) print("}", file=sf, end="") else: print(";", file=sf, end="") return sf.getvalue()
[docs] @dataclass class TransitionDefinition(ASTNode): """ Represents a transition definition in the state machine DSL. Transitions define how the state machine moves from one state to another in response to events and conditions. :param from_state: The source state name or :data:`INIT_STATE` singleton :type from_state: Union[str, _StateSingletonMark] :param to_state: The target state name or :data:`EXIT_STATE` singleton :type to_state: Union[str, _StateSingletonMark] :param event_id: Optional event identifier that triggers the transition :type event_id: Optional[ChainID] :param condition_expr: Optional condition expression that must be true for the transition :type condition_expr: Optional[Expr] :param post_operations: List of operation statements to perform after the transition :type post_operations: List[OperationalStatement] :rtype: TransitionDefinition Example:: >>> init_trans = TransitionDefinition(INIT_STATE, "idle", None, None, []) >>> event_trans = TransitionDefinition( ... "idle", "active", ChainID(["idle", "start"]), None, [] ... ) >>> op = OperationAssignment("counter", Integer("0")) >>> cond_trans = TransitionDefinition( ... "active", "idle", None, ... BinaryOp(Name("counter"), ">", Integer("10")), ... [op], ... ) """ from_state: Union[str, _StateSingletonMark] to_state: Union[str, _StateSingletonMark] event_id: Optional[ChainID] condition_expr: Optional[Expr] post_operations: List["OperationalStatement"] _span: Optional[Span] = field(default=None, repr=False, compare=False)
[docs] def __str__(self) -> str: """ Convert the transition definition to its string representation. :return: String representation of the transition definition :rtype: str """ with io.StringIO() as sf: print( "[*]" if self.from_state is INIT_STATE else self.from_state, file=sf, end="", ) print(" -> ", file=sf, end="") print( "[*]" if self.to_state is EXIT_STATE else self.to_state, file=sf, end="" ) if self.event_id is not None: if not self.event_id.is_absolute and ( (self.from_state is INIT_STATE and len(self.event_id.path) == 1) or ( self.from_state is not INIT_STATE and len(self.event_id.path) == 2 and self.event_id.path[0] == self.from_state ) ): print(f" :: {self.event_id.path[-1]}", file=sf, end="") else: print(f" : {self.event_id}", file=sf, end="") elif self.condition_expr is not None: print(f" : if [{self.condition_expr}]", file=sf, end="") if len(self.post_operations) > 0: print(" effect {", file=sf) print( _render_operational_statement_block(self.post_operations), file=sf, end="", ) print("}", file=sf, end="") else: print(";", file=sf, end="") return sf.getvalue()
[docs] @dataclass class ForceTransitionDefinition(ASTNode): """ Represents a forced transition definition in the state machine DSL. Forced transitions override normal transitions and are used for special cases like error handling or interrupts. :param from_state: The source state name or :data:`ALL` singleton :type from_state: Union[str, _StateSingletonMark] :param to_state: The target state name or :data:`EXIT_STATE` singleton :type to_state: Union[str, _StateSingletonMark] :param event_id: Optional event identifier that triggers the transition :type event_id: Optional[ChainID] :param condition_expr: Optional condition expression that must be true for the transition :type condition_expr: Optional[Expr] :rtype: ForceTransitionDefinition Example:: >>> force_trans = ForceTransitionDefinition(ALL, "error", None, None) >>> str(force_trans) '! * -> error;' """ from_state: Union[str, _StateSingletonMark] to_state: Union[str, _StateSingletonMark] event_id: Optional[ChainID] condition_expr: Optional[Expr] _span: Optional[Span] = field(default=None, repr=False, compare=False)
[docs] def __str__(self) -> str: """ Convert the force transition definition to its string representation. :return: String representation of the force transition definition :rtype: str """ with io.StringIO() as sf: print("! ", file=sf, end="") print("*" if self.from_state is ALL else self.from_state, file=sf, end="") print(" -> ", file=sf, end="") print( "[*]" if self.to_state is EXIT_STATE else self.to_state, file=sf, end="" ) if self.event_id is not None: if not self.event_id.is_absolute and ( (self.from_state is ALL and len(self.event_id.path) == 1) or ( self.from_state is not ALL and len(self.event_id.path) == 2 and self.event_id.path[0] == self.from_state ) ): print(f" :: {self.event_id.path[-1]}", file=sf, end="") else: print(f" : {self.event_id}", file=sf, end="") elif self.condition_expr is not None: print(f" : if [{self.condition_expr}]", file=sf, end="") print(";", file=sf, end="") return sf.getvalue()
[docs] @dataclass class StateDefinition(ASTNode): """ Represents a state definition in the state machine DSL. States are the fundamental building blocks of state machines, containing transitions, substates, and actions to be performed on entry, during, and exit. :param name: The name of the state :type name: str :param extra_name: Optional additional name for the state :type extra_name: Optional[str] :param events: List of events defined within this state :type events: List[EventDefinition] :param imports: List of import statements declared within this state :type imports: List[ImportStatement] :param substates: List of nested state definitions :type substates: List[StateDefinition] :param transitions: List of transitions from this state :type transitions: List[TransitionDefinition] :param enters: List of actions to perform when entering the state :type enters: List[EnterStatement] :param durings: List of actions to perform while in the state :type durings: List[DuringStatement] :param exits: List of actions to perform when exiting the state :type exits: List[ExitStatement] :param during_aspects: List of aspect-specific actions to perform while in the state :type during_aspects: List[DuringAspectStatement] :param force_transitions: List of forced transitions from this state :type force_transitions: List[ForceTransitionDefinition] :param is_pseudo: Whether this is a pseudo state :type is_pseudo: bool :rtype: StateDefinition Example:: >>> simple_state = StateDefinition("idle") >>> str(simple_state) 'state idle;' >>> trans = TransitionDefinition( ... "idle", "active", ChainID(["idle", "start"]), None, [] ... ) >>> state_with_trans = StateDefinition("idle", transitions=[trans]) """ name: str extra_name: Optional[str] = None events: List["EventDefinition"] = None imports: List["ImportStatement"] = None substates: List["StateDefinition"] = None transitions: List[TransitionDefinition] = None enters: List["EnterStatement"] = None durings: List["DuringStatement"] = None exits: List["ExitStatement"] = None during_aspects: List["DuringAspectStatement"] = None force_transitions: List["ForceTransitionDefinition"] = None is_pseudo: bool = False _span: Optional[Span] = field(default=None, repr=False, compare=False)
[docs] def __post_init__(self) -> None: """ Initialize default empty lists for optional parameters. """ self.events = self.events or [] self.imports = self.imports or [] self.substates = self.substates or [] self.transitions = self.transitions or [] self.force_transitions = self.force_transitions or [] self.enters = self.enters or [] self.durings = self.durings or [] self.exits = self.exits or [] self.during_aspects = self.during_aspects or []
[docs] def __str__(self) -> str: """ Convert the state definition to its string representation. :return: String representation of the state definition :rtype: str """ with io.StringIO() as sf: if self.is_pseudo: print("pseudo ", file=sf, end="") print(f"state {self.name}", file=sf, end="") if self.extra_name is not None: print(f" named {self.extra_name!r}", file=sf, end="") if ( not self.substates and not self.transitions and not self.force_transitions and not self.events and not self.imports and not self.enters and not self.durings and not self.exits and not self.during_aspects ): print(f";", file=sf, end="") else: print(f" {{", file=sf) for enter_item in self.enters: print(indent(str(enter_item), prefix=" "), file=sf) for during_item in self.durings: print(indent(str(during_item), prefix=" "), file=sf) for exit_item in self.exits: print(indent(str(exit_item), prefix=" "), file=sf) for during_aspect_item in self.during_aspects: print(indent(str(during_aspect_item), prefix=" "), file=sf) for import_item in self.imports: print(indent(str(import_item), prefix=" "), file=sf) for substate in self.substates: print(indent(str(substate), prefix=" "), file=sf) for event in self.events: print(indent(str(event), prefix=" "), file=sf) for force_transition in self.force_transitions: print(indent(str(force_transition), prefix=" "), file=sf) for transition in self.transitions: print(indent(str(transition), prefix=" "), file=sf) print(f"}}", file=sf, end="") return sf.getvalue()
[docs] @dataclass class OperationAssignment(OperationalStatement): """ Represents an operation assignment in the state machine DSL. Operation assignments are used to modify variables during transitions or state actions. :param name: The name of the variable :type name: str :param expr: The expression defining the new value :type expr: Expr :rtype: OperationAssignment Example:: >>> op_assign = OperationAssignment( ... "counter", BinaryOp(Name("counter"), "+", Integer("1")) ... ) >>> str(op_assign) 'counter = counter + 1;' """ name: str expr: Expr
[docs] def __str__(self) -> str: """ Convert the operation assignment to its string representation. :return: String representation of the operation assignment :rtype: str """ return f"{self.name} = {self.expr};"
[docs] @dataclass class OperationIfBranch(ASTNode): """ Represents a single branch inside an operation-block ``if`` statement. :param condition: Branch condition, or ``None`` for the final ``else`` branch. :type condition: Optional[Expr] :param statements: Statements executed when the branch is selected. :type statements: List[OperationalStatement] :rtype: OperationIfBranch """ condition: Optional[Expr] statements: List[OperationalStatement]
[docs] @dataclass class OperationIf(OperationalStatement): """ Represents an ``if / else if / else`` statement inside an operation block. :param branches: Ordered branch list. The last branch may have ``condition=None`` to represent ``else``. :type branches: List[OperationIfBranch] :rtype: OperationIf """ branches: List[OperationIfBranch]
[docs] def __str__(self) -> str: """ Convert the operation ``if`` statement to its DSL representation. :return: String representation of the operation ``if`` statement. :rtype: str """ with io.StringIO() as sf: first_branch = self.branches[0] print(f"if [{first_branch.condition}] {{", file=sf) for index, branch in enumerate(self.branches): if index > 0: if branch.condition is None: print("} else {", file=sf) else: print(f"}} else if [{branch.condition}] {{", file=sf) print( _render_operational_statement_block(branch.statements), file=sf, end="", ) print("}", file=sf, end="") return sf.getvalue()
[docs] @dataclass class EventDefinition(ASTNode): """ Represents an event definition in the state machine DSL. Events are signals that can trigger transitions or other actions within the state machine. :param name: The name of the event :type name: str :param extra_name: Optional additional name for the event :type extra_name: Optional[str] :rtype: EventDefinition Example:: >>> event = EventDefinition("start") >>> str(event) 'event start;' >>> named_event = EventDefinition("start", "Start Event") >>> str(named_event) 'event start named "Start Event";' """ name: str extra_name: Optional[str] = None
[docs] def __str__(self) -> str: """ Convert the event definition to its string representation. :return: String representation of the event definition :rtype: str """ with io.StringIO() as sf: print(f"event {self.name}", file=sf, end="") if self.extra_name is not None: print(f" named {self.extra_name!r}", file=sf, end="") print(";", file=sf, end="") return sf.getvalue()
[docs] @dataclass class StateMachineDSLProgram(ASTNode): """ Represents a complete state machine DSL program. A program consists of variable definitions and a root state that contains the entire state machine hierarchy. :param definitions: List of variable definitions :type definitions: List[DefAssignment] :param root_state: The root state of the state machine :type root_state: StateDefinition :rtype: StateMachineDSLProgram Example:: >>> def_var = DefAssignment("counter", "int", Integer("0")) >>> root = StateDefinition("root") >>> program = StateMachineDSLProgram([def_var], root) >>> print(str(program)) def int counter = 0; state root; """ definitions: List[DefAssignment] root_state: StateDefinition
[docs] def __str__(self) -> str: """ Convert the state machine program to its string representation. :return: String representation of the state machine program :rtype: str """ with io.StringIO() as f: for definition in self.definitions: print(definition, file=f) print(self.root_state, file=f, end="") return f.getvalue()
[docs] @dataclass class EnterStatement(ASTNode): """ Abstract base class for enter statements in the state machine DSL. Enter statements define actions to be performed when entering a state. :rtype: EnterStatement """ pass
[docs] @dataclass class EnterOperations(EnterStatement): """ Represents a block of operations to perform when entering a state. :param operations: List of operation statements :type operations: List[OperationalStatement] :param name: Optional name for the operation block :type name: Optional[str] :rtype: EnterOperations Example:: >>> op = OperationAssignment("counter", Integer("0")) >>> enter_ops = EnterOperations([op]) >>> print(str(enter_ops)) enter { counter = 0; } """ operations: List[OperationalStatement] name: Optional[str] = None
[docs] def __str__(self) -> str: """ Convert the enter operations to their string representation. :return: String representation of the enter operations :rtype: str """ with io.StringIO() as f: if self.name: print(f"enter {self.name} {{", file=f) else: print(f"enter {{", file=f) print(_render_operational_statement_block(self.operations), file=f, end="") print("}", file=f, end="") return f.getvalue()
[docs] @dataclass class EnterAbstractFunction(EnterStatement): """ Represents an abstract function to call when entering a state. Abstract functions are placeholders for implementation-specific behavior. :param name: Optional name of the function :type name: Optional[str] :param doc: Optional documentation for the function :type doc: Optional[str] :rtype: EnterAbstractFunction Example:: >>> enter_func = EnterAbstractFunction("initState", "Initialize the state") >>> print(str(enter_func)) enter abstract initState /* Initialize the state */ """ name: Optional[str] doc: Optional[str]
[docs] def __str__(self) -> str: """ Convert the enter abstract function to its string representation. :return: String representation of the enter abstract function :rtype: str """ with io.StringIO() as f: if self.name: print(f"enter abstract {self.name}", file=f, end="") else: print(f"enter abstract", file=f, end="") if self.doc is not None: print(" /*", file=f) print(indent(self.doc, prefix=" "), file=f) print("*/", file=f, end="") else: print(";", file=f, end="") return f.getvalue()
[docs] @dataclass class EnterRefFunction(EnterStatement): """ Represents a reference function to call when entering a state. Reference functions point to existing functions defined elsewhere. :param name: Optional name of the function :type name: Optional[str] :param ref: Chain identifier referencing the function :type ref: ChainID :rtype: EnterRefFunction Example:: >>> ref_func = EnterRefFunction("init", ChainID(["common", "initialize"])) >>> str(ref_func) 'enter init ref common.initialize;' """ name: Optional[str] ref: ChainID
[docs] def __str__(self) -> str: """ Convert the enter reference function to its string representation. :return: String representation of the enter reference function :rtype: str """ if self.name: return f"enter {self.name} ref {self.ref};" else: return f"enter ref {self.ref};"
[docs] @dataclass class ExitStatement(ASTNode): """ Abstract base class for exit statements in the state machine DSL. Exit statements define actions to be performed when exiting a state. :rtype: ExitStatement """ pass
[docs] @dataclass class ExitOperations(ExitStatement): """ Represents a block of operations to perform when exiting a state. :param operations: List of operation statements :type operations: List[OperationalStatement] :param name: Optional name for the operation block :type name: Optional[str] :rtype: ExitOperations Example:: >>> op = OperationAssignment("active", Boolean("false")) >>> exit_ops = ExitOperations([op]) >>> print(str(exit_ops)) exit { active = false; } """ operations: List[OperationalStatement] name: Optional[str] = None
[docs] def __str__(self) -> str: """ Convert the exit operations to their string representation. :return: String representation of the exit operations :rtype: str """ with io.StringIO() as f: if self.name: print(f"exit {self.name} {{", file=f) else: print(f"exit {{", file=f) print(_render_operational_statement_block(self.operations), file=f, end="") print("}", file=f, end="") return f.getvalue()
[docs] @dataclass class ExitAbstractFunction(ExitStatement): """ Represents an abstract function to call when exiting a state. Abstract functions are placeholders for implementation-specific behavior. :param name: Optional name of the function :type name: Optional[str] :param doc: Optional documentation for the function :type doc: Optional[str] :rtype: ExitAbstractFunction Example:: >>> exit_func = ExitAbstractFunction("cleanupState", "Clean up resources") >>> print(str(exit_func)) exit abstract cleanupState /* Clean up resources */ """ name: Optional[str] doc: Optional[str]
[docs] def __str__(self) -> str: """ Convert the exit abstract function to its string representation. :return: String representation of the exit abstract function :rtype: str """ with io.StringIO() as f: if self.name: print(f"exit abstract {self.name}", file=f, end="") else: print(f"exit abstract", file=f, end="") if self.doc is not None: print(" /*", file=f) print(indent(self.doc, prefix=" "), file=f) print("*/", file=f, end="") else: print(";", file=f, end="") return f.getvalue()
[docs] @dataclass class ExitRefFunction(ExitStatement): """ Represents a reference function to call when exiting a state. Reference functions point to existing functions defined elsewhere. :param name: Optional name of the function :type name: Optional[str] :param ref: Chain identifier referencing the function :type ref: ChainID :rtype: ExitRefFunction Example:: >>> ref_func = ExitRefFunction("cleanup", ChainID(["common", "cleanup"])) >>> str(ref_func) 'exit cleanup ref common.cleanup;' """ name: Optional[str] ref: ChainID
[docs] def __str__(self) -> str: """ Convert the exit reference function to its string representation. :return: String representation of the exit reference function :rtype: str """ if self.name: return f"exit {self.name} ref {self.ref};" else: return f"exit ref {self.ref};"
[docs] @dataclass class DuringStatement(ASTNode): """ Abstract base class for during statements in the state machine DSL. During statements define actions to be performed while in a state. :rtype: DuringStatement """ pass
[docs] @dataclass class DuringOperations(DuringStatement): """ Represents a block of operations to perform while in a state. :param aspect: Optional aspect name (e.g., ``"entry"``, ``"do"``, ``"exit"``) :type aspect: Optional[str] :param operations: List of operation statements :type operations: List[OperationalStatement] :param name: Optional name for the operation block :type name: Optional[str] :rtype: DuringOperations Example:: >>> op = OperationAssignment("counter", BinaryOp(Name("counter"), "+", Integer("1"))) >>> during_ops = DuringOperations("do", [op]) >>> print(str(during_ops)) during do { counter = counter + 1; } """ aspect: Optional[str] operations: List[OperationalStatement] name: Optional[str] = None
[docs] def __str__(self) -> str: """ Convert the during operations to their string representation. :return: String representation of the during operations :rtype: str """ with io.StringIO() as f: if self.name: if self.aspect: print(f"during {self.aspect} {self.name} {{", file=f) else: print(f"during {self.name} {{", file=f) else: if self.aspect: print(f"during {self.aspect} {{", file=f) else: print(f"during {{", file=f) print(_render_operational_statement_block(self.operations), file=f, end="") print("}", file=f, end="") return f.getvalue()
[docs] @dataclass class DuringAbstractFunction(DuringStatement): """ Represents an abstract function to call while in a state. Abstract functions are placeholders for implementation-specific behavior. :param name: Optional name of the function :type name: Optional[str] :param aspect: Optional aspect name (e.g., ``"entry"``, ``"do"``, ``"exit"``) :type aspect: Optional[str] :param doc: Optional documentation for the function :type doc: Optional[str] :rtype: DuringAbstractFunction Example:: >>> during_func = DuringAbstractFunction("processData", "do", "Process incoming data") >>> print(str(during_func)) during do abstract processData /* Process incoming data */ """ name: Optional[str] aspect: Optional[str] doc: Optional[str]
[docs] def __str__(self) -> str: """ Convert the during abstract function to its string representation. :return: String representation of the during abstract function :rtype: str """ with io.StringIO() as f: if self.name: if self.aspect: print(f"during {self.aspect} abstract {self.name}", file=f, end="") else: print(f"during abstract {self.name}", file=f, end="") else: if self.aspect: print(f"during {self.aspect} abstract", file=f, end="") else: print(f"during abstract", file=f, end="") if self.doc is not None: print(" /*", file=f) print(indent(self.doc, prefix=" "), file=f) print("*/", file=f, end="") else: print(";", file=f, end="") return f.getvalue()
[docs] @dataclass class DuringRefFunction(DuringStatement): """ Represents a reference function to call while in a state. Reference functions point to existing functions defined elsewhere. :param name: Optional name of the function :type name: Optional[str] :param aspect: Optional aspect name (e.g., ``"entry"``, ``"do"``, ``"exit"``) :type aspect: Optional[str] :param ref: Chain identifier referencing the function :type ref: ChainID :rtype: DuringRefFunction Example:: >>> ref_func = DuringRefFunction("process", "do", ChainID(["common", "process"])) >>> str(ref_func) 'during do process ref common.process;' """ name: Optional[str] aspect: Optional[str] ref: ChainID
[docs] def __str__(self) -> str: """ Convert the during reference function to its string representation. :return: String representation of the during reference function :rtype: str """ if self.name: if self.aspect: return f"during {self.aspect} {self.name} ref {self.ref};" else: return f"during {self.name} ref {self.ref};" else: if self.aspect: return f"during {self.aspect} ref {self.ref};" else: return f"during ref {self.ref};"
[docs] @dataclass class DuringAspectStatement(ASTNode): """ Abstract base class for during aspect statements in the state machine DSL. During aspect statements define aspect-specific actions to be performed while in a state. :rtype: DuringAspectStatement """ pass
[docs] @dataclass class DuringAspectOperations(DuringAspectStatement): """ Represents a block of aspect-specific operations to perform while in a state. :param aspect: The aspect name (e.g., ``"before"``, ``"after"``) :type aspect: str :param operations: List of operation statements :type operations: List[OperationalStatement] :param name: Optional name for the operation block :type name: Optional[str] :rtype: DuringAspectOperations Example:: >>> op = OperationAssignment("counter", BinaryOp(Name("counter"), "+", Integer("1"))) >>> during_ops = DuringAspectOperations("before", [op]) >>> print(str(during_ops)) >> during before { counter = counter + 1; } """ aspect: str operations: List[OperationalStatement] name: Optional[str] = None
[docs] def __str__(self) -> str: """ Convert the during aspect operations to their string representation. :return: String representation of the during aspect operations :rtype: str """ with io.StringIO() as f: if self.name: print(f">> during {self.aspect} {self.name} {{", file=f) else: print(f">> during {self.aspect} {{", file=f) print(_render_operational_statement_block(self.operations), file=f, end="") print("}", file=f, end="") return f.getvalue()
[docs] @dataclass class DuringAspectAbstractFunction(DuringAspectStatement): """ Represents an abstract function to call for a specific aspect while in a state. Abstract functions are placeholders for implementation-specific behavior. :param name: Optional name of the function :type name: Optional[str] :param aspect: The aspect name (e.g., ``"before"``, ``"after"``) :type aspect: str :param doc: Optional documentation for the function :type doc: Optional[str] :rtype: DuringAspectAbstractFunction Example:: >>> during_func = DuringAspectAbstractFunction( ... "processData", "before", "Process incoming data" ... ) >>> print(str(during_func)) >> during before abstract processData /* Process incoming data */ """ name: Optional[str] aspect: str doc: Optional[str]
[docs] def __str__(self) -> str: """ Convert the during aspect abstract function to its string representation. :return: String representation of the during aspect abstract function :rtype: str """ with io.StringIO() as f: if self.name: print(f">> during {self.aspect} abstract {self.name}", file=f, end="") else: print(f">> during {self.aspect} abstract", file=f, end="") if self.doc is not None: print(" /*", file=f) print(indent(self.doc, prefix=" "), file=f) print("*/", file=f, end="") else: print(";", file=f, end="") return f.getvalue()
[docs] @dataclass class DuringAspectRefFunction(DuringAspectStatement): """ Represents a reference function to call for a specific aspect while in a state. Reference functions point to existing functions defined elsewhere. :param name: Optional name of the function :type name: Optional[str] :param aspect: The aspect name (e.g., ``"before"``, ``"after"``) :type aspect: str :param ref: Chain identifier referencing the function :type ref: ChainID :rtype: DuringAspectRefFunction Example:: >>> ref_func = DuringAspectRefFunction( ... "process", "before", ChainID(["common", "process"]) ... ) >>> str(ref_func) '>> during before process ref common.process;' """ name: Optional[str] aspect: str ref: ChainID
[docs] def __str__(self) -> str: """ Convert the during aspect reference function to its string representation. :return: String representation of the during aspect reference function :rtype: str """ if self.name: return f">> during {self.aspect} {self.name} ref {self.ref};" else: return f">> during {self.aspect} ref {self.ref};"