v0.46.0 — Structured Expression Capture (Capture-Don't-Evaluate)¶
Status: - Phase 0 (prerequisite) — DONE in v0.48.0: assert constraint / constraint / calc / state / action / requirement / satisfy / allocation usages inside
part def,item def, etc. bodies now survivePart.load_from_grammarinto the public-API tree. 10 regression tests intests/grammar_test.py::*v046_phase0*. - Phase 1 (expression refactor) — DONE in v0.52.0: visitor now emits a structured expression dict with operators captured at the correct layer, using a precedence-climbing pass to recover the correct operator order from the left-associative ANTLR parse. 10 regression tests intests/grammar_test.py::*v046_phase1(binary equality, invocation, feature chain, arithmetic precedence, unary minus,notoperator, logicaland/or(text-preservation fallback), conditional ternary (layer-presence), range, structural non-collapse). Known limitations: -and/or/xor/impliesfall back to text preservation (the grammar classes use list-based operand/operator fields that don't fit the chain layout). -a.bfield access falls back to text preservation (the grammar class lacks aFeatureChainExpressionthatBaseExpressioncan read). - The grammar'sIF ? :ternary form is not currently parseable insidecalcbodies (known grammar limitation). Author: Context from issue #3 triage + codebase investigation. Target release: v0.48.0 (Phase 0), v0.52.0 (Phase 1) Estimated effort: ~1 week (see §8).
1. Problem¶
Today, _visit_owned_expression (src/sysmlpy/antlr_visitor.py:10557) collapses
every non-literal expression into a single FeatureReferenceMember whose
memberElement is a QualifiedName with the whole expression text as a
single name part. Example:
assert constraint { radius == zero }
# visitor emits:
# FeatureReferenceExpression
# FeatureReferenceMember
# QualifiedName { names: ["radius==zero"] } # ← entire text, structure lost
The wrapping EqualityExpression / AdditiveExpression / … chain in
_wrap_expression_layers is emitted with empty operator/operand arrays for
every binary layer, so the operator is never captured. Round-trip works only
because QualifiedName.dump() returns the raw text verbatim.
This makes a future name resolver infeasible: a resolver needs individually
addressable FeatureReferenceExpression nodes (one per identifier) so it can
look each up in the symbol table. With the current lossy collapse, the resolver
would have to re-parse raw text — duplicating grammar work and drifting from
the ANTLR grammar.
A second, related bug blocks testing: assert constraint inside a part def
body is dropped by Part.load_from_grammar in src/sysmlpy/usage.py:782.
That dispatch handles PartDefinition / ItemUsage / AttributeUsage / etc.
but has no branch for AssertConstraintUsage / ConstraintUsage /
CalculationUsage. The visitor emits the assert constraint correctly; the
grammar classes construct it correctly; usage.py discards it when building
the public-API Part tree. So model.dump() silently omits every assert
constraint inside a part. This must be fixed first so expression-capture tests
can even observe the round-trip.
2. Goal¶
Replace the lossy single-FeatureReferenceMember collapse with a structured
expression dict that preserves operator structure, so a future v0.47+ name
resolver can walk identifiers individually.
In scope:
- Prerequisite: plumb AssertConstraintUsage / ConstraintUsage /
CalculationUsage / StateUsage / ActionUsage through
Part.load_from_grammar so they survive into the public-API Part.children.
- Refactor _visit_owned_expression to walk the ANTLR ownedExpression
subtree and emit per-rule dicts keyed on the expression kind
(BinaryExpression, InvocationExpression, FeatureChainExpression,
LiteralInteger, LiteralReal, LiteralString, LiteralBoolean,
FeatureReferenceExpression, MetadataAccessExpression,
ConstructorExpression, CastExpression, ConditionalExpression,
UnaryExpression, ParenthesizedExpression, IfExpression).
- Add matching grammar classes in grammar/classes.py for any new dict shapes
that don't already have a class (most already exist — see §4).
- Public API: Model.find_expressions() helper that walks
assertConstraintUsage / calculationUsage / requirementUsage and yields
(constraint, expression) pairs.
- Documentation: README + STATUS + CHANGELOG.
Out of scope (deferred to v0.47+): - Name resolution (looking up identifiers in the symbol table). - Type checking (verifying operand types are compatible). - OCL evaluation (evaluating the expression to a value). - Constraint-body semantic checks (the "check" half of "parse + check").
3. Architecture: the capture layer¶
SysML text
→ ANTLR4 parse tree (ownedExpression context tree)
→ _visit_owned_expression (REFACTORED)
→ structured OwnedExpression dict
→ grammar/classes.py (OwnedExpression / ConditionalExpression / …)
→ dump() / get_definition() round-trip
→ (future v0.47+) semantic.py walks the structured dict, resolves names
The refactor touches only the visitor emission and the grammar-class
construction. Everything downstream (the OwnedExpression /
ConditionalExpression / EqualityExpression / … class hierarchy in
grammar/classes.py:5539-6350) already exists and already round-trips — the
problem is that the visitor never fills the operator/operand arrays, so
the classes get empty data.
4. Existing grammar classes (most already exist)¶
grammar/classes.py already has the full expression class hierarchy
(built when the visitor emits the empty-chain shape from
_wrap_expression_layers):
| Class | Line | Status |
|---|---|---|
OwnedExpression |
5539 | exists, works |
ConditionalExpression |
5554 | exists, but dump() just joins operands — won't render ? : until visitor fills operators |
NullCoalescingExpression |
5572 | exists |
ImpliesExpression |
5599 | exists |
OrExpression |
5626 | exists |
XorExpression |
5653 | exists |
AndExpression |
5727 | exists |
EqualityExpression |
5773 | exists — needs operator rendering in dump() |
ClassificationExpression |
5796 | exists |
RelationalExpression |
5841 | exists — needs operator rendering |
RangeExpression |
5865 | exists |
AdditiveExpression |
5910 | exists — needs operator rendering |
MultiplicativeExpression |
5955 | exists — needs operator rendering |
ExponentiationExpression |
5975 | exists |
UnaryExpression |
6002 | exists |
ExtentExpression |
6029 | exists |
PrimaryExpression |
6067 | exists, fully working (handles [], (), ., ->, #) |
BaseExpression |
6250 | exists, dispatches FeatureReferenceExpression / LiteralInteger / LiteralReal / LiteralString / SequenceExpression / InvocationExpression |
FeatureReferenceExpression |
6399 | exists |
FeatureReferenceMember |
6419 | exists |
SequenceExpression |
6224 | exists |
BodyExpression |
6176-ish | exists |
InvocationExpression |
~6310 | exists |
ArgumentList / PositionalArgumentList / NamedArgumentList / ArgumentMember / Argument |
~6330+ | exist |
LiteralInteger / LiteralReal / LiteralString |
8107-8145 | exist |
LiteralInfinity |
~8145 | exists |
LiteralBoolean |
grep for it | may need adding |
FeatureChainMember |
6176 | exists |
Net new classes likely needed: LiteralBoolean (if absent), and possibly
CastExpression / ConstructorExpression / MetadataAccessExpression /
IfExpression if the current PrimaryExpression / BaseExpression dispatch
doesn't already cover them. Audit during implementation; do not add classes
that already exist.
Net new behavior needed: the dump() methods on
EqualityExpression / RelationalExpression / AdditiveExpression /
MultiplicativeExpression / ExponentiationExpression / UnaryExpression /
ConditionalExpression must render the operator between operands. Today they
just join operands (no operator text), because the visitor never fills the
operator arrays. After the refactor, the visitor fills the operators, and
dump() must emit lhs OP rhs instead of lhsrhs.
5. Implementation plan¶
Phase 0 — Prerequisite: assert constraint survives Part.load_from_grammar¶
File: src/sysmlpy/usage.py (Usage.load_from_grammar, line 782).
The children-dispatch loop (lines ~920-1078) handles PartDefinition,
ItemDefinition, PartUsage, ItemUsage, AttributeUsage,
StructureUsageElement, StructureDefinitionElement, Definition. It does
NOT handle AssertConstraintUsage, ConstraintUsage, CalculationUsage,
StateUsage, ActionUsage, RequirementUsage, SatisfyRequirementUsage,
FlowConnectionUsage (as children of a part), ConnectionUsage (as child of
part), InterfaceUsage, etc.
Fix: add a branch for each missing usage kind. For AssertConstraintUsage
/ ConstraintUsage / CalculationUsage, construct a Constraint (or
Calculation) public-API object, set .grammar = inner_element, extract name
from the declaration if present, set .parent = self, append to
self.children. Mirror the pattern already used at the package level in
definition.py:1269-1290.
Risk: Part.dump() and Attribute.usage_dump() rebuild the body via
_get_definition(child="DefinitionBody") — need to verify the rebuilt dict
includes the assert constraint. If Constraint doesn't have a
_get_definition that emits the right DefinitionBodyItem shape, the
round-trip will drop it. Check Constraint._get_definition (in usage.py)
and extend if needed.
Tests (failing-first, commit before the visitor refactor):
- test_assert_constraint_survives_part_def_load_gh3_prereq:
part def W { attribute r; assert constraint { r == 0 } } →
model.dump() contains assert constraint.
- test_constraint_usage_survives_part_def_load_gh3_prereq:
part def W { constraint c { r == 0 } } → model.dump() contains
constraint c.
- test_calculation_usage_survives_part_def_load_gh3_prereq:
part def W { calc mass { sum(parts.mass) } } → model.dump() contains
calc mass.
Phase 1 — Visitor refactor: structured expression emission¶
File: src/sysmlpy/antlr_visitor.py.
Replace _visit_owned_expression (line 10557) with a dispatcher that walks
the ANTLR ownedExpression context tree and fills the existing expression
chain with real operator/operand data instead of empty arrays.
Key insight: the ANTLR ownedExpression rule is left-recursive with
operator precedence (see SysMLv2Parser.g4:22-62). ANTLR parses
radius == zero as:
ownedExpression
: ownedExpression EQ_EQ ownedExpression # the == alternative
↳ lhs: ownedExpression (baseExpression)
↳ rhs: ownedExpression (baseExpression)
ANTLR's Python runtime exposes this as: oe_ctx.getChildCount() == 3,
oe_ctx.getChild(0) is the lhs ownedExpression, oe_ctx.getChild(1) is
the EQ_EQ terminal, oe_ctx.getChild(2) is the rhs ownedExpression.
For unary prefix (-x, not x): getChildCount() == 2,
getChild(0) is the operator terminal, getChild(1) is the operand.
For the base case (baseExpression): oe_ctx.baseExpression() is non-null.
New helper functions (add near _visit_owned_expression):
def _emit_expression(oe_ctx):
"""Walk an ownedExpression context and return a structured OwnedExpression
dict. Dispatches on ANTLR child shape: binary operator, unary operator,
conditional (if/then/else), or base expression."""
...
def _emit_binary_expression(lhs_ctx, op_terminal, rhs_ctx):
"""Fill the EqualityExpression / RelationalExpression / AdditiveExpression /
MultiplicativeExpression / ExponentiationExpression / RangeExpression /
AndExpression / OrExpression / XorExpression / ImpliesExpression /
NullCoalescingExpression / ConditionalExpression layers with real operator
and operand data."""
...
def _emit_unary_expression(op_terminal, operand_ctx):
"""Fill the UnaryExpression layer with the operator and operand."""
...
def _emit_conditional_expression(if_ctx, then_ctx, else_ctx):
"""Fill the ConditionalExpression layer with condition/then/else."""
...
def _emit_base_expression(base_ctx):
"""Dispatch a baseExpression to the right primary: literal, feature
reference, invocation, constructor, body expression, cast, or
parenthesized expression."""
...
Strategy for _emit_binary_expression: the existing
_wrap_expression_layers builds the empty chain. Reuse it as a skeleton, but
instead of passing a single primary_expression at the bottom, walk the lhs
and rhs recursively and splice them into the right operator layer. Map the
operator terminal to the layer:
| Operator terminal(s) | Expression layer (existing class) |
|---|---|
EQ_EQ, BANG_EQ, EQ_EQ_EQ, BANG_EQ_EQ |
EqualityExpression |
LT, GT, LE, GE |
RelationalExpression |
PLUS, MINUS |
AdditiveExpression |
STAR, SLASH, PERCENT |
MultiplicativeExpression |
STAR_STAR, CARET |
ExponentiationExpression |
DOT_DOT |
RangeExpression |
AMP |
AndExpression |
PIPE |
OrExpression |
XOR |
XorExpression |
IMPLIES |
ImpliesExpression |
QUESTION_QUESTION |
NullCoalescingExpression |
IF / QUESTION / ELSE (ternary) |
ConditionalExpression |
For each binary node, emit:
{
"name": "<LayerName>Expression", # e.g. "EqualityExpression"
"operator": [op_text], # e.g. ["=="]
"operand": [lhs_dict, rhs_dict], # two operands, recursively structured
# ...nested layers below this one, filled by recursing into lhs/rhs...
}
Critical: keep the existing nested-layer structure (the
ConditionalExpression → NullCoalescingExpression → ImpliesExpression → … →
PrimaryExpression chain) so existing grammar classes still work. The change
is that the operator/operand arrays are now populated with real data
instead of empty.
Backstop: if _emit_expression can't recognize a shape (defensive — should
not happen for valid SysML), fall back to the existing
_make_feature_reference_primary(text) path. Print a [expression] unhandled
X warning (mirrors the AGENTS.md catch-all pattern). This preserves the
graceful-fallback contract.
Phase 2 — Grammar class dump() fixes¶
File: src/sysmlpy/grammar/classes.py.
The existing dump() methods on EqualityExpression, RelationalExpression,
AdditiveExpression, MultiplicativeExpression, ExponentiationExpression,
UnaryExpression, ConditionalExpression currently join operands with no
operator text (because the visitor never filled operators). After Phase 1,
the visitor fills operators, so dump() must emit lhs OP rhs.
Pattern (apply to each binary-expression class):
def dump(self):
parts = []
for i, operand in enumerate(self.operands):
parts.append(operand.dump())
if i < len(self.operator):
parts.append(self.operator[i]) # the operator string
return " ".join(parts) # or "" .join, matching existing whitespace
Audit each class's existing dump() and adjust. Verify against the
failing-first tests from Phase 0 + new expression regression tests.
For UnaryExpression:
def dump(self):
if self.operator:
return f"{self.operator} {self.operand.dump()}" # or no space, per grammar
return self.operand.dump()
For ConditionalExpression (if/then/else):
def dump(self):
# if cond ? then : else → SysML syntax is "if cond then x else y"
# Verify the exact SysML v2 syntax for conditional expressions.
Risk: whitespace. Existing tests use strip_ws(text) == strip_ws(dump()),
so whitespace differences are tolerated. But new tests should use exact
whitespace where it matters for readability.
Phase 3 — Public API¶
File: src/sysmlpy/__init__.py (or a new src/sysmlpy/expressions.py
module, exported from __init__.py).
Add:
def find_expressions(model) -> list[tuple[Any, Any]]:
"""Walk a model and yield (container, expression) pairs for every
assert constraint, constraint usage, and calculation usage body.
Each `expression` is the structured OwnedExpression dict (the visitor
output). A future name resolver (v0.47+) can consume this directly.
Returns
-------
list[tuple[Any, dict]]
List of (container_element, expression_dict) pairs.
"""
...
Walk the model: for each Part / Attribute / Constraint / Calculation /
Requirement in model.children (recursively), find
AssertConstraintUsage / ConstraintUsage / CalculationUsage in
.grammar.body (or .grammar.definition.body), drill into
ResultExpressionMember.ownedRelatedElement, yield the pair.
Tests:
- test_find_expressions_returns_structured_gh3: for a model with
assert constraint { radius == zero }, find_expressions(model) returns a
list whose first element's expression dict contains an
EqualityExpression with operator: ["=="] and two
FeatureReferenceExpression operands (radius, zero).
Phase 4 — Regression tests¶
File: tests/grammar_test.py (add at the end, near the issue #3 tests).
Add these failing-first tests, commit them BEFORE the refactor so they document the expected end state:
def test_expression_capture_binary_regression_gh3_capture():
"""`radius == zero` must round-trip with the operator captured."""
text = """package P {
part def W {
attribute r;
assert constraint { r == zero }
}
}"""
a = loads(text)
dumped = classtree(a).dump()
assert "==" in dumped, f"binary operator lost; got: {dumped!r}"
assert "r" in dumped and "zero" in dumped
def test_expression_capture_invocation_regression_gh3_capture():
"""`size(edges) == 18` must round-trip with the invocation captured."""
text = """package P {
part def Shape {
attribute edges;
assert constraint { size(edges) == 18 }
}
}"""
a = loads(text)
dumped = classtree(a).dump()
assert "size(edges)" in dumped or "size ( edges )" in dumped
assert "==" in dumped and "18" in dumped
def test_expression_capture_chain_regression_gh3_capture():
"""`wheel1.mass` must round-trip as a feature chain, not raw text."""
text = """package P {
part def Vehicle {
part wheel1 { attribute mass; }
assert constraint { wheel1.mass > 0 }
}
}"""
a = loads(text)
dumped = classtree(a).dump()
assert "wheel1" in dumped and "mass" in dumped
assert ">" in dumped and "0" in dumped
def test_expression_capture_arithmetic_regression_gh3_capture():
"""`a + b * c` must round-trip with precedence preserved."""
text = """package P {
part def E {
attribute a; attribute b; attribute c;
assert constraint { a + b * c == 0 }
}
}"""
a = loads(text)
dumped = classtree(a).dump()
assert "+" in dumped and "*" in dumped and "==" in dumped
def test_expression_capture_unary_regression_gh3_capture():
"""`-x` and `not flag` must round-trip with the unary operator captured."""
text = """package P {
part def E {
attribute x; attribute flag;
assert constraint { -x == 0 and not flag }
}
}"""
a = loads(text)
dumped = classtree(a).dump()
assert "-" in dumped and "not" in dumped
Also add a structural test (not just text round-trip) to lock in the dict shape so a future resolver has something to walk:
def test_expression_dict_has_equality_layer_gh3_capture():
"""The visitor must emit a populated EqualityExpression, not an empty one
with the whole text collapsed into a FeatureReferenceMember."""
from sysmlpy.antlr_parser import parse
from sysmlpy.antlr_visitor import _visit_root_namespace_dict
text = """package P {
part def W {
attribute r;
assert constraint { r == zero }
}
}"""
d = _visit_root_namespace_dict(parse(text))
def find_first(node, target):
if isinstance(node, dict):
if node.get("name") == target:
return node
for v in node.values():
r = find_first(v, target) if isinstance(v, (dict, list)) else None
if r: return r
elif isinstance(node, list):
for it in node:
r = find_first(it, target) if isinstance(it, (dict, list)) else None
if r: return r
return None
eq = find_first(d, "EqualityExpression")
assert eq is not None, "EqualityExpression layer missing entirely"
assert len(eq.get("operator", [])) >= 1, \
f"EqualityExpression has no operator; got: {eq!r}"
assert eq["operator"][0] == "==", \
f"EqualityExpression operator is not '=='; got: {eq['operator']!r}"
Phase 5 — Documentation¶
- README.md: update the scope reminder (added in v0.45.0) — note that expressions are now captured structurally even if names aren't yet resolved. Remove the strongest form of "parse ≠ check"; replace with "names inside expressions are now captured as structured trees; resolution is future work".
- STATUS.md: move the
semantic.pyconstraint-body entry from "Known Issues" to "Remaining Work" with the v0.46.0 intermediate milestone noted. Add a new "Known Issues" entry: "Expression operators are captured butdump()whitespace for binary operators may differ from the source (whitespace-normalized round-trip still passes)". - CHANGELOG.md: v0.46.0 entry.
- AGENTS.md: add a "Expression capture" section pointing at the new
_emit_expressionhelpers and the dict shapes, so future agents know the contract.
Phase 6 — Verify¶
poetry run pytest tests/grammar_test.py --tb=short -q— all grammar tests pass (existing 103 + new ~6).poetry run pytest tests/class_test.py tests/main_test.py tests/repr_test.py tests/navigate_test.py tests/semantic_test.py tests/partial_test.py tests/redefined_name_test.py tests/import_test.py --tb=short -q— full fast suite passes (412+ existing + new).poetry run pytest -m conformance --tb=no -q— 118/123 (same 5 pre-existing failures, byte-identical).- If any previously-passing test fails, STOP and investigate (per AGENTS.md "if output changes for a passing test, stop").
6. Risk mitigations¶
| Risk | Mitigation |
|---|---|
Existing strip_ws(text) == strip_ws(dump()) tests break |
Each emitter preserves child text via getText() or by recursing into structured children. Whitespace behavior shouldn't change. If a test breaks, STOP. |
| Deep operator precedence double-emits | Write the deeply-nested test (a + b * c == 0) FIRST, before refactoring. Commit it as the regression bar. |
| 354 grammar classes — adding new ones is small | Audit first; most classes already exist. Only add what's missing. |
Part.load_from_grammar prerequisite fix breaks other tests |
The fix only ADDS branches for previously-dropped usage kinds; it doesn't change existing branches. Existing tests for parts/items/attributes are unaffected. |
dump() whitespace for binary operators differs from source |
Tolerated by strip_ws comparison. Document in STATUS.md. |
| The visitor refactor is large | Keep _wrap_expression_layers as the skeleton builder; only change what gets spliced into the operator/operand arrays. Mechanical change. |
| Conformance baseline shifts | Must stay at 118/123 with byte-identical failures. If it shifts, STOP. |
7. What this enables (v0.47+)¶
A name resolver becomes feasible because every name reference inside an
expression is now an individually-addressable FeatureReferenceExpression
(with its FeatureChainExpression chain) rather than buried inside raw text.
v0.47+ name resolver sketch:
1. Walk assertConstraintUsage → calculationBody → resultExpressionMember
→ ownedExpression (recursive).
2. At each FeatureReferenceExpression, resolve the qualifiedName against
the SymbolTable (already built by semantic.py).
3. At each FeatureChainExpression, resolve the source's type, then resolve
each chain step on that type (extending _check_feature_chaining_compatible
in semantic.py:1800).
4. Emit SemanticIssues for unresolved names.
v0.48+ type checker: verify operand types are compatible for each binary
operator (e.g. == requires matching types, + requires numeric types).
v0.49+ OCL evaluation: evaluate the expression to a value (for assert
constraint verification).
This is what gets you from "parse-clean but not checked" to "parse-clean AND names checked". Type-checking / OCL evaluation remain out of scope for v0.47.
8. Effort estimate¶
| Phase | Effort |
|---|---|
Phase 0 (prerequisite: Part.load_from_grammar assert constraint) |
~1 day |
Phase 1 (visitor refactor: _emit_expression + helpers) |
~3-4 days |
Phase 2 (grammar class dump() fixes) |
~1 day |
Phase 3 (public API: find_expressions) |
~0.5 day |
| Phase 4 (regression tests) | ~0.5 day (write failing-first BEFORE Phase 1) |
| Phase 5 (documentation) | ~0.5 day |
| Phase 6 (verify + fix regressions) | ~1 day |
| Total | ~7-8 days |
9. File pointers (quick reference)¶
| What | Where |
|---|---|
| Lossy visitor function | src/sysmlpy/antlr_visitor.py:10557 (_visit_owned_expression) |
| Empty-chain skeleton builder | src/sysmlpy/antlr_visitor.py:10352 (_wrap_expression_layers) |
| Primary builders | src/sysmlpy/antlr_visitor.py:10434-10535 (_make_literal_integer_primary, etc.) |
| Expression grammar | src/sysmlpy/grammar/antlr4/SysMLv2Parser.g4:22-135 (ownedExpression, baseExpression, etc.) |
| Expression grammar classes | src/sysmlpy/grammar/classes.py:5539-6350 (OwnedExpression through BaseExpression) |
Part.load_from_grammar (prerequisite fix) |
src/sysmlpy/usage.py:782 |
Part.load_from_grammar children dispatch (missing assert) |
src/sysmlpy/usage.py:920-1078 |
| Package-level assert dispatch (already works) | src/sysmlpy/definition.py:1269 |
AssertConstraintUsage grammar class |
src/sysmlpy/grammar/classes.py:3373 |
CalculationBody grammar class |
src/sysmlpy/grammar/classes.py:3434 |
ResultExpressionMember grammar class |
src/sysmlpy/grammar/classes.py:3531 |
DefinitionBodyItem dispatch |
src/sysmlpy/grammar/classes.py:4935 |
OccurrenceUsageElement dispatch |
src/sysmlpy/grammar/classes.py:6439 |
BehaviorUsageElement dispatch |
src/sysmlpy/grammar/classes.py:3663 |
| Issue #3 (this work unblocks finding 4) | https://github.com/mycr0ft/sysmlpy/issues/3 |
| AGENTS.md expression-capture section (to add) | AGENTS.md (new section after "Common Pitfalls") |
10. Open questions for implementation¶
LiteralBooleanclass: does it already exist ingrammar/classes.py? If not, add it (mirrorsLiteralInteger). Check during Phase 1.CastExpression/ConstructorExpression/MetadataAccessExpression/IfExpression: are these already reachable viaPrimaryExpression.ownedRelationship1/ownedRelationship2, or do they need new grammar classes? Audit during Phase 1; the ANTLR grammar hasconstructorExpression,metadataAccessExpression, and theIF … QUESTION … ELSEconditional alternative. The existingPrimaryExpressionclass already handles[],(),.,->,#— these may be sufficient. Verify.- Whitespace in
dump(): should binary operators be rendered aslhs OP rhs(with spaces) orlhsOPrhs(no spaces)? SysML v2 textual notation typically uses spaces around binary operators. Check the OMG examples and match. Thestrip_wstest comparator tolerates either. FeatureChainExpressionvsPrimaryExpression.ownedRelationship1: the existingPrimaryExpressionclass handles.chains viaownedRelationship1/ownedRelationship2(FeatureChainMember). Is a separateFeatureChainExpressionclass needed, or doesPrimaryExpressionalready cover it? Audit during Phase 1; likelyPrimaryExpressionis sufficient and no new class is needed.Part.load_from_grammarprerequisite: should the new branches live inusage.py:920(insidePart.load_from_grammar) or be hoisted into a shared helper called by bothPart.load_from_grammarandPackage.load_from_grammar? The package-level dispatch indefinition.py:1269already handlesAssertConstraintUsage; consider extracting a shared_load_usage_child(self, inner_element)helper to avoid duplication. Decide during Phase 0.