Source code for longeron.model

"""In-memory model of SysML v2 elements.

This is a pragmatic projection of the SysML v2 metamodel: typed dataclasses
with enough structure to faithfully capture every construct of the textual
notation (packages, definitions, usages, expressions, actions, states,
requirements, views, metadata) while staying small.  The builder maps each
construct to a typed element with no lossy fallback; :class:`Unsupported`
remains only as a defensive dead-end that the test suite asserts is never
produced.

All elements can also be constructed programmatically::

    pkg = Package(name="Vehicles")
    part = Definition(kind="part", name="Vehicle")
    part.add(Usage(kind="attribute", name="mass", types=["Real"],
                   value=FeatureValue(Literal(1500.0))))
    pkg.add(part)
"""

from __future__ import annotations

from collections.abc import Iterator
from dataclasses import dataclass, field
from typing import Literal, get_args

from .ast import Expr
from .ast import Literal as LiteralExpr

ENTRY_SOURCE = "<entry>"  # sentinel source for entry-transitions in states

#: the public surface of the model vocabulary -- star-imported by
#: :mod:`longeron` itself, so this list is deliberate and explicit: closed
#: vocabularies and element classes only (imports like ``typing.Literal``
#: or ``dataclasses.field`` must never leak into ``longeron.*``).
__all__ = [
    "DEFINITION_KINDS",
    "USAGE_KINDS",
    "AcceptAction",
    "Alias",
    "AllocationUsage",
    "AssignmentAction",
    "BindingConnector",
    "Comment",
    "ConnectionUsage",
    "ConnectorEnd",
    "ConstraintKind",
    "ControlNode",
    "ControlNodeKind",
    "Definition",
    "DefinitionKind",
    "Dependency",
    "Direction",
    "Documentation",
    "Element",
    "ElementFilter",
    "EnumerationDefinition",
    "Expose",
    "FeatureValue",
    "FlowUsage",
    "ForLoop",
    "IfAction",
    "Import",
    "InitialNode",
    "InterfaceUsage",
    "MetadataUsage",
    "MetadataValue",
    "Model",
    "Multiplicity",
    "Namespace",
    "Package",
    "PerformAction",
    "PortionKind",
    "SatisfyUsage",
    "SendAction",
    "StateAction",
    "StateActionKind",
    "Succession",
    "TerminateAction",
    "TextualRepresentation",
    "TransitionUsage",
    "TriggerKind",
    "Unsupported",
    "Usage",
    "UsageKind",
    "Visibility",
    "WhileLoop",
]

# ---------------------------------------------------------------------------
# Closed vocabularies
# ---------------------------------------------------------------------------

Visibility = Literal["public", "private", "protected"]
Direction = Literal["in", "out", "inout", "return"]
PortionKind = Literal["snapshot", "timeslice"]
ConstraintKind = Literal["assume", "require", "assert"]
TriggerKind = Literal["at", "after", "when"]
ControlNodeKind = Literal["merge", "decision", "join", "fork"]
StateActionKind = Literal["entry", "do", "exit"]

#: definition ``kind`` values map 1:1 to declaration keywords
DefinitionKind = Literal[
    "part",
    "item",
    "attribute",
    "port",
    "action",
    "calc",
    "constraint",
    "requirement",
    "concern",
    "state",
    "occurrence",
    "individual",
    "enum",
    "connection",
    "flow",
    "allocation",
    "metadata",
    "rendering",
    "case",
    "analysis",
    "verification",
    "use_case",
    "view",
    "viewpoint",
    "interface",
    "extended",
]

#: usage ``kind`` values; mostly declaration keywords plus a few synthetic
#: ones (``feature`` for keyword-less usages, ``enum_literal``, ...)
UsageKind = Literal[
    "part",
    "item",
    "attribute",
    "port",
    "ref",
    "feature",
    "enum",
    "enum_literal",
    "occurrence",
    "individual",
    "snapshot",
    "timeslice",
    "event",
    "event_occurrence",
    "action",
    "calc",
    "constraint",
    "requirement",
    "concern",
    "state",
    "case",
    "analysis",
    "verification",
    "use_case",
    "subject",
    "actor",
    "stakeholder",
    "objective",
    "connection",
    "binding",
    "interface",
    "allocation",
    "flow",
    "message",
    "view",
    "viewpoint",
    "rendering",
    "render",
    "satisfy",
    "verify",
    "frame",
    "include",
    "extended",
]

DEFINITION_KINDS: tuple[str, ...] = get_args(DefinitionKind)
USAGE_KINDS: tuple[str, ...] = get_args(UsageKind)


# ---------------------------------------------------------------------------
# Core
# ---------------------------------------------------------------------------


[docs] @dataclass(eq=False) class Element: """Base class for every model element.""" name: str | None = None short_name: str | None = None visibility: Visibility | None = None metadata: list[str] = field(default_factory=list) # '#keyword' prefixes owner: Element | None = field(default=None, repr=False, compare=False) #: build-time :class:`~longeron.errors.SourceLocation` (or ``None``). #: Set as a plain instance attribute by the builder -- deliberately #: not a dataclass field, so serialization and text round-trips stay #: position-free. Read by lint diagnostics. source_location = None #: the ``.sysml`` file this element was loaded from when a directory #: workspace was merged (:func:`longeron.workspace.load_dir` stamps its #: per-file models' top-level members), or ``None``. A plain instance #: attribute like ``source_location`` -- deliberately not a dataclass #: field, so JSON exports and text round-trips stay file-agnostic. #: Read by :func:`longeron.export.save_workspace` (per-file save-back). source_file = None @property def label(self) -> str: return self.name or self.short_name or f"<anonymous {type(self).__name__}>" @property def qualified_name(self) -> str | None: own = self.name or self.short_name if own is None: return None parts = [own] node = self.owner while node is not None: node_name = node.name or node.short_name if node_name: parts.append(node_name) node = node.owner return "::".join(reversed(parts)) def iter_tree(self) -> Iterator[Element]: yield self for child in self.children(): yield from child.iter_tree() def children(self) -> list[Element]: return []
[docs] @dataclass(eq=False) class Namespace(Element): """An element that owns other elements.""" members: list[Element] = field(default_factory=list) def add(self, *elements: Element) -> Namespace: for element in elements: element.owner = self self.members.append(element) return self def children(self) -> list[Element]: return list(self.members) def member_named(self, name: str) -> Element | None: for member in self.members: if name in (member.name, member.short_name): return member for member in self.members: if isinstance(member, Alias) and member.name == name: return self.member_named(member.target.split("::")[-1]) return None
[docs] def find(self, qualified: str) -> Element | None: """Naive descent through owned members by ``::``-separated path.""" node: Element | None = self for part in qualified.split("::"): if not isinstance(node, Namespace): return None node = node.member_named(part) if node is None: return None return node
@property def doc(self) -> str | None: texts = [m.text for m in self.members if isinstance(m, Documentation)] return "\n".join(texts) if texts else None
[docs] @dataclass(eq=False) class Model(Namespace): """The root namespace of one or more parsed sources.""" source_name: str | None = None
[docs] @dataclass(eq=False) class Package(Namespace): is_library: bool = False is_standard: bool = False
# --------------------------------------------------------------------------- # Relationships & annotations # ---------------------------------------------------------------------------
[docs] @dataclass(eq=False) class Import(Element): target: str = "" is_namespace: bool = False # 'X::*' is_recursive: bool = False # '::**' is_import_all: bool = False # 'import all ...' filters: list[Expr] = field(default_factory=list) # 'import X::*[@F];'
[docs] @dataclass(eq=False) class Alias(Element): target: str = ""
[docs] @dataclass(eq=False) class Comment(Element): body: str = "" # raw '/* ... */' text about: list[str] = field(default_factory=list) locale: str | None = None @property def text(self) -> str: return _strip_comment_body(self.body)
[docs] @dataclass(eq=False) class Documentation(Element): body: str = "" # raw '/* ... */' text locale: str | None = None @property def text(self) -> str: return _strip_comment_body(self.body)
[docs] @dataclass(eq=False) class TextualRepresentation(Element): language: str = "" body: str = ""
[docs] @dataclass(eq=False) class Dependency(Element): clients: list[str] = field(default_factory=list) suppliers: list[str] = field(default_factory=list)
[docs] @dataclass(eq=False) class Unsupported(Element): """A construct the builder does not model; ``text`` is verbatim source.""" text: str = "" rule: str = ""
def _strip_comment_body(body: str) -> str: text = body.strip() if text.startswith("/*"): text = text[2:] if text.endswith("*/"): text = text[:-2] lines = [line.strip().lstrip("*").strip() for line in text.strip().splitlines()] return "\n".join(lines).strip() # --------------------------------------------------------------------------- # Values & multiplicity # ---------------------------------------------------------------------------
[docs] @dataclass class FeatureValue: expr: Expr = field(default_factory=lambda: LiteralExpr(None)) is_default: bool = False # 'default =' is_initial: bool = False # ':='
[docs] @dataclass class Multiplicity: lower: Expr | None = None upper: Expr | None = None is_ordered: bool = False is_nonunique: bool = False
[docs] @dataclass class ConnectorEnd: target: str = "" name: str | None = None # 'name ::> target' form multiplicity: Multiplicity | None = None # cross multiplicity '[m..n]'
# --------------------------------------------------------------------------- # Definitions and usages # ---------------------------------------------------------------------------
[docs] @dataclass(eq=False) class Definition(Namespace): kind: DefinitionKind = "part" is_abstract: bool = False is_variation: bool = False is_individual: bool = False is_parallel: bool = False # state definitions supers: list[str] = field(default_factory=list) # ':>' specializations result: Expr | None = None # calc/constraint/case result expression
[docs] @dataclass(eq=False) class EnumerationDefinition(Definition): kind: DefinitionKind = "enum" @property def literals(self) -> list[Usage]: return [m for m in self.members if isinstance(m, Usage) and m.kind == "enum_literal"]
[docs] @dataclass(eq=False) class Usage(Namespace): kind: UsageKind = "part" direction: Direction | None = None is_abstract: bool = False is_variation: bool = False is_variant: bool = False is_readonly: bool = False # 'constant' is_derived: bool = False is_ref: bool = False is_end: bool = False is_individual: bool = False is_parallel: bool = False # state usages is_negated: bool = False # 'assert not' is_exhibit: bool = False # 'exhibit state ...' portion_kind: PortionKind | None = None types: list[str] = field(default_factory=list) # ': T' subsets: list[str] = field(default_factory=list) # ':> f' redefines: list[str] = field(default_factory=list) # ':>> f' references: str | None = None # '::> f' crosses: str | None = None # '=> f' multiplicity: Multiplicity | None = None value: FeatureValue | None = None result: Expr | None = None # calc/constraint usage result expression constraint_kind: ConstraintKind | None = None
[docs] @dataclass(eq=False) class ConnectionUsage(Usage): kind: UsageKind = "connection" ends: list[ConnectorEnd] = field(default_factory=list)
[docs] @dataclass(eq=False) class BindingConnector(Usage): kind: UsageKind = "binding" source_end: ConnectorEnd | None = None target_end: ConnectorEnd | None = None
[docs] @dataclass(eq=False) class SatisfyUsage(Usage): """``satisfy R by system;`` / ``assert not satisfy ...``""" kind: UsageKind = "satisfy" is_assert: bool = False by: str | None = None
# --------------------------------------------------------------------------- # Behavior: action statements # ---------------------------------------------------------------------------
[docs] @dataclass(eq=False) class AssignmentAction(Element): target: str = "" # dotted feature path expr: Expr = field(default_factory=lambda: LiteralExpr(None))
[docs] @dataclass(eq=False) class IfAction(Element): condition: Expr = field(default_factory=lambda: LiteralExpr(True)) then_body: list[Element] = field(default_factory=list) else_body: list[Element] | IfAction | None = None def children(self) -> list[Element]: extra: list[Element] = [] if isinstance(self.else_body, IfAction): extra = [self.else_body] elif self.else_body: extra = list(self.else_body) return list(self.then_body) + extra
[docs] @dataclass(eq=False) class WhileLoop(Element): condition: Expr | None = None # None => 'loop' body: list[Element] = field(default_factory=list) until: Expr | None = None def children(self) -> list[Element]: return list(self.body)
[docs] @dataclass(eq=False) class ForLoop(Element): var: str = "" seq: Expr = field(default_factory=lambda: LiteralExpr(None)) body: list[Element] = field(default_factory=list) def children(self) -> list[Element]: return list(self.body)
[docs] @dataclass(eq=False) class SendAction(Element): payload: Expr = field(default_factory=lambda: LiteralExpr(None)) via: Expr | None = None to: Expr | None = None
[docs] @dataclass(eq=False) class AcceptAction(Element): payload_name: str | None = None payload_types: list[str] = field(default_factory=list) trigger_kind: TriggerKind | None = None trigger: Expr | None = None via: Expr | None = None
[docs] @dataclass(eq=False) class PerformAction(Element): target: str | None = None # referenced action action: Usage | None = None # inline 'perform action x { ... }' def children(self) -> list[Element]: return [self.action] if self.action is not None else []
[docs] @dataclass(eq=False) class TerminateAction(Element): target: Expr | None = None
[docs] @dataclass(eq=False) class ControlNode(Element): kind: ControlNodeKind = "merge"
[docs] @dataclass(eq=False) class InitialNode(Element): target: str = "" # 'first <target>;'
[docs] @dataclass(eq=False) class Succession(Element): source: str | None = None # None => attached to previous member target: str = "" guard: Expr | None = None is_else: bool = False
# --------------------------------------------------------------------------- # States # ---------------------------------------------------------------------------
[docs] @dataclass(eq=False) class StateAction(Element): """``entry ...;`` / ``do ...;`` / ``exit ...;`` inside a state.""" kind: StateActionKind = "entry" action: Element | None = None # Perform/Assignment/Send/Accept or None def children(self) -> list[Element]: return [self.action] if self.action is not None else []
[docs] @dataclass(eq=False) class TransitionUsage(Element): source: str | None = None trigger: AcceptAction | None = None guard: Expr | None = None effect: Element | None = None target: str = "" def children(self) -> list[Element]: out: list[Element] = [] if self.trigger is not None: out.append(self.trigger) if self.effect is not None: out.append(self.effect) return out
# --------------------------------------------------------------------------- # Interfaces, views, flows, metadata # ---------------------------------------------------------------------------
[docs] @dataclass(eq=False) class InterfaceUsage(Usage): """``interface i : I connect a.p to b.q { ... }``""" kind: UsageKind = "interface" ends: list[ConnectorEnd] = field(default_factory=list)
[docs] @dataclass(eq=False) class AllocationUsage(Usage): """``allocate a to b`` / ``allocation al allocate a to b``""" kind: UsageKind = "allocation" ends: list[ConnectorEnd] = field(default_factory=list)
[docs] @dataclass(eq=False) class FlowUsage(Usage): """``flow of Payload from a.out to b.in`` (also message / succession flow).""" kind: UsageKind = "flow" payload: str | None = None # payload feature rendering source: str | None = None target_end: str | None = None is_succession: bool = False # 'succession flow'
[docs] @dataclass(eq=False) class ElementFilter(Element): """``filter <expr>;`` in packages and views.""" condition: Expr = field(default_factory=lambda: LiteralExpr(True))
[docs] @dataclass(eq=False) class Expose(Element): """``expose X::*;`` inside a view.""" target: str = "" is_namespace: bool = False is_recursive: bool = False filters: list[Expr] = field(default_factory=list) # 'expose X::**[@F];'
[docs] @dataclass(eq=False) class MetadataUsage(Namespace): """``@Safety { level = 3; }`` annotating usage (also ``metadata ...``).""" typed_by: str = "" about: list[str] = field(default_factory=list)
[docs] @dataclass(eq=False) class MetadataValue(Element): """A body member of a metadata usage: ``level = 3;`` or ``:>> f = v;``.""" redefines: str = "" value: FeatureValue | None = None nested: list[MetadataValue] = field(default_factory=list) def children(self) -> list[Element]: return list(self.nested)