From 4f88e67833eef6a421d33e76a120856216a6cf0f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 18:53:51 +0000 Subject: [PATCH 1/3] feat(runtime): materialize binding and flow usages as connector objects Co-Authored-By: jason.han --- .../binding-flow-connector-objects.added.md | 1 + docs/project/spec-compliance.md | 7 +- internal/check/passes/typecheck.go | 9 +- internal/exec/runtime/adopt.go | 2 +- internal/exec/runtime/classifier_behavior.go | 4 +- internal/exec/runtime/classify_test.go | 4 +- internal/exec/runtime/conformance_test.go | 13 +- internal/exec/runtime/connector.go | 11 +- internal/exec/runtime/connector_test.go | 141 ++++++++++++++++++ internal/exec/runtime/instance.go | 4 +- .../robustness_connector_objects_test.go | 95 ++++++++++++ .../exec/runtime/routing_differential_test.go | 57 +++++++ internal/exec/runtime/subsetting.go | 2 +- ...onnector_object_binding_flow.expected.json | 14 ++ .../connector_object_binding_flow.sysml | 14 ++ internal/exec/runtime/variation.go | 2 +- internal/frontend/repl/complete.go | 2 +- internal/semantic/semantics/connector.go | 73 ++++++++- internal/semantic/semantics/connector_test.go | 84 +++++++++++ internal/semantic/semantics/shape.go | 4 + 20 files changed, 512 insertions(+), 31 deletions(-) create mode 100644 changes/unreleased/binding-flow-connector-objects.added.md create mode 100644 internal/exec/runtime/robustness_connector_objects_test.go create mode 100644 internal/exec/runtime/routing_differential_test.go create mode 100644 internal/exec/runtime/testdata/conformance/connector_object_binding_flow.expected.json create mode 100644 internal/exec/runtime/testdata/conformance/connector_object_binding_flow.sysml diff --git a/changes/unreleased/binding-flow-connector-objects.added.md b/changes/unreleased/binding-flow-connector-objects.added.md new file mode 100644 index 000000000..eec10232a --- /dev/null +++ b/changes/unreleased/binding-flow-connector-objects.added.md @@ -0,0 +1 @@ +- **Binding and non-message flow usages now materialize as connector objects.** Their ends hold the connected feature values through the same connector path used by `connect` usages; message flows and one-ended bindings remain excluded. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index d54876dba..4c5dc8bfc 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -1921,9 +1921,9 @@ semantics layer over the conjugation parity of the typing/specialization chain. | The `perform X;` shorthand is an action node named X | `lower/action_graph.go` `getNodeName` | `conformance/action_perform_shorthand.sysml` (`succession first start then increment;` names the perform statement) | ✅ Faithful | | N-ary connector ends (`connection link connect (a, b, c)`), SysML v2 7.13.2, 8.3.13 | `parser/defusage.go` `parseConnectorEnds` (parenthesized end list, reached by both the named declaration and the anonymous `connect …;` body member) over `parseConnectorEnd`, which takes exactly one target per end and only the relationship forms the `ConnectorEnd` production allows (`::>`, `references`, an explicit `:>>`), so a comma separates ends instead of being read as a further relationship of the first one; `passes/constraint.go` `checkConnectorEnds` (arity by kind, the interface case gated on `semantics.Model.IsBinaryConnector`, since `Interfaces::Interface` is n-ary — `ref port :>> participant : Port[2..*]` — and only `BinaryInterface` narrows it to two); `semantics/implicit.go` gives exactly two-ended untyped interface/connection definitions and usages their binary bases; `lower/connection.go` `lowerConnections`, `PeerPorts` | `parse/connection_nary.golden`, `parser/connector_ends_nary_test.go` `TestParseNaryConnectorEndsKeepsEveryEnd`, `parser/negative_test.go` (`nary_connect_unclosed`, `nary_connect_trailing_comma`, `nary_connect_empty`), `passes/constraint_test.go` `TestConstraintConnectionNaryEndCountReachesTheChecker`, `passes/w9c_rules_test.go` `TestW9CBinaryInterfaceEndDiamondWarns`, `TestW9CNonBinaryConnectorEndsStaySilent`, `TestConstraintBinaryInterfaceNaryFails`, `lower/connection_test.go` `TestLowerNaryConnectionKeepsEveryEnd` and `TestLowerAnonymousNaryConnectionKeepsEveryEnd`, `parser/connector_ends_nary_test.go` `TestParseAnonymousInlineConnectKeepsEveryEnd`, conformance `action_port_communication_nary.sysml` and `action_port_communication_nary_anonymous.sysml` | ✅ Faithful: SysML v2 §7.14.1 permits three or more ends on a general interface; §7.14.2 and §8.3.14.2 constrain `BinaryInterface`. Exactly two-ended untyped interfaces and connections receive binary typing, an explicit binary interface with too many ends errors, and a three-ended general interface remains general. The universal `InterfaceUsage_Invalid.sysml.xt:49` expectation is a pilot limitation | | Anonymous binary allocation (`allocate torqueGenerator to powerTrain`) | `parser/defusage.go` `atAllocateShorthand` | `parse/perform_reference.golden` | ✅ Faithful (both names are connector ends; formerly the first was read as the usage's name) | -| An object of a connector usage holds the features it connects at its ends (`connection link : Link connect a.p to b.q` makes `link.source` **be** `a.p`), KerML 7.4.6, SysML v2 7.13.2 | `runtime/connector.go` `materializeConnectorFeatureValue`, `materializeConnector`, `attachConnectorEnd`, `bindEndFeatureValue`, `bindParticipants`, reached from `runtime/instance.go` `GetFeatureValue`; end features synthesized by `runtime/shape.go` `connectorEndFeatures`; attachments and effective end names by `semantics/connector.go` `Model.ConnectorEndAttachments`, `Model.IsConnectorUsage`; inherited ends aliased by `runtime/subsetting.go` over `Model.ImplicitEndRedefinitions` | `connector_test.go` (`TestConnectorEndsAreTheConnectedFeatures`, `TestWritingAConnectedPortIsReadThroughTheEnd`, `TestConnectorEndFollowsAFeatureChain`, `TestConnectorEndAttachesToAPart`, `TestNaryConnectorKeepsEveryEnd`, `TestRedefinedEndSharesTheInheritedFeatureValue`, `TestEveryConnectorKindAttachesItsEnds`), `conformance/connector_end_identity.sysml` (identity assertions), `semantics/connector_test.go:TestConnectorEndAttachments`, `robustness_test.go:unattachable_connector_end`, `multiplicity_on_a_connector`, `connector_attached_to_itself`, `mutually_attached_connectors` | ✅ Faithful (an end holds the very object the connector attaches to, so writing the connected port is read through the end and two connectors on different ports are distinguishable; ends are attached in declaration order, including n-ary and nested feature chains and an end attached to a part; an end that names no reachable feature is a typed `ErrConnectorEnd` with a source location rather than a fresh object or ``, an end naming the connector it belongs to — directly or through another connector — is `ErrCyclicFeatureValue`, and a connector usage holding more than one connector is reported with where it was written) | -| An untyped or anonymous connector usage materializes on the standard library base of its kind (`interface iface connect a.p to b.q;`, `connect a.p to b.q;`), SysML v2 7.13.2, 8.3.13 | `semantics/implicit.go` `implicitUsageBases` (`Connections::Connection`, `Interfaces::Interface`, `Allocations::Allocation`); `runtime/connector.go` `connectorBaseOf`, `anonymousConnectors`; `symbols/builder.go` `usageSymbolKind` (a KerML `connector` is a connection usage) and `semantics/shape.go` `IsShapeFeature` (an allocation usage is a feature) | `conformance/connector_end_identity.sysml`, `ballandchain_interface_connected.sysml`, `connector_test.go` (`TestUntypedConnectorUsageMaterializes`, `TestAnonymousConnectorJoinsItsEnds`, `TestAnonymousConnectorIsMaterializedOnce`, `TestAnonymousSuccessionIsNoConnector`, `TestEveryConnectorKindAttachesItsEnds`), `parse/connection_implicit_type.golden` | ✅ Faithful (a connection, interface, allocation or connector usage that names no definition is an object of its kind's library base with its ends attached, named form and anonymous form alike, and an anonymous one materializes once per object; a flow or binding states its ends by other syntax and is not a `connect` connector — its ends reach routing through lowering, not through connector-end feature values) | -| A flow usage (`flow f from a.out to b.in`) and a binding usage (`binding b bind a.p = b.p`) are connectors of the kernel layer, but state their ends in their own syntax — `Usage.FlowEnds`, and a binding's two `Usage.ConnectorEnds` — rather than in a `connect` clause | `parser/defusage.go` `parseFlowEnds` and `parseBindingDeclaration`/`parseBindingEnds`; `resolve/document.go` `isImplicitCalcResult`; `lower/connection.go` (flow ends reach routing through lowering); `lower/binding.go` `ToBindings`/`lowerBinding`; `runtime/binding.go` `objectBindings`/`resolveBindingValue`/`resolveBindingSet`/`attemptBinding`/`unmaterializedObjectEnd`/`resolveBindingLocation`; `runtime/instance.go` `materializeFeatureValue`; `runtime/invoke_calc.go` `resultBindingExpr`; `semantics/connector.go` `Model.IsConnectorUsage` deliberately covers only the `connect` forms | `parse/connection_implicit_type.golden` (`flow f from a.p to b.p;`, `binding bnd bind a.p = b.p;`), `lower/connection_test.go`, `lower/binding_test.go` (`TestToBindingsKeepsMultipleContributors`), `runtime/testdata/conformance/binding_value_forward.sysml`, `binding_value_reverse.sysml`, `binding_nested_end.sysml`, `binding_nested_end_reverse.sysml`, `binding_expression_end.sysml`, `binding_multivalued.sysml`, `binding_calc_result.sysml`, `binding_calc_result_reverse.sysml`, `binding_object_end.sysml`, `binding_chained_object_ends.sysml`, `runtime/robustness_test.go` binding cases (including exact collection-conflict, multiple-contributor, element-budget and distinct-object cases) | ⚠️ Approximate (a flow between action nodes carries its value through lowering; a binding declared in a materialized type/usage body is lowered to a bidirectional runtime value identity, including inherited and nested ends, exact sequence/set equality, multiplicity handling, element-budget charging during propagation, lazy adoption of an unmaterialized composite endpoint by the read-side object — the end being read adopts an object the other end already holds rather than building a fresh one, so a chain of bindings through nested assemblies stays one object whichever end is read first —, conflict/cycle errors, and calc result binding. A binding that states only one end — `bind x;`, `binding bnd of x;` — lowers to no runtime binding (`binding bnd = x;` states two ends, `bnd` and `x`, per KerML.xtext BindingConnectorDeclaration), and bindings owned directly by packages/namespaces are not applied until namespace objects are materialized. Several bindings supplying unequal scalar values report a typed conflict; multiple bindings contributing to a multi-valued end are not yet element-wise merged and report a typed `ErrBindingEnd` instead of silently selecting one. **Adjudicated: the resolution half agrees with the reference** — `flow f from a.p.o to b.p.i;` with feature-chain ends is clean in the pinned validator and in ours (`passes/w6c_row_adjudication_test.go:TestW6CFlowChainEndsAndOccurrenceModifiers`); the residual is the runtime's binding merge, in `runtime/`, which this change did not own.) | +| An object of a connector usage holds the features it connects at its ends (`connection link : Link connect a.p to b.q` makes `link.source` **be** `a.p`), KerML 7.4.6, SysML v2 7.13.2 | `runtime/connector.go` `materializeConnectorFeatureValue`, `materializeConnector`, `attachConnectorEnd`, `bindEndFeatureValue`, `bindParticipants`, reached from `runtime/instance.go` `GetFeatureValue`; end features synthesized by `runtime/shape.go` `connectorEndFeatures`; attachments and effective end names by `semantics/connector.go` `Model.ConnectorObjectEnds`, `Model.IsConnectorObjectUsage`; inherited ends aliased by `runtime/subsetting.go` over `Model.ImplicitEndRedefinitions` | `connector_test.go` (`TestConnectorEndsAreTheConnectedFeatures`, `TestWritingAConnectedPortIsReadThroughTheEnd`, `TestConnectorEndFollowsAFeatureChain`, `TestConnectorEndAttachesToAPart`, `TestNaryConnectorKeepsEveryEnd`, `TestRedefinedEndSharesTheInheritedFeatureValue`, `TestEveryConnectorKindAttachesItsEnds`, `TestBindingConnectorIsAnObjectOfItsEnds`, `TestFlowConnectorIsAnObjectOfItsEnds`, `TestBindingConnectorEndFollowsAFeatureChain`, `TestBindingConnectorEndsHoldBoundValues`), `conformance/connector_end_identity.sysml`, `conformance/connector_object_binding_flow.sysml`, `semantics/connector_test.go:TestConnectorEndAttachments`, `semantics/connector_test.go:TestConnectorObjectEnds`, `robustness_connector_objects_test.go`, `robustness_test.go:unattachable_connector_end`, `multiplicity_on_a_connector`, `connector_attached_to_itself`, `mutually_attached_connectors` | ✅ Faithful (an end holds the very object the connector attaches to, so writing the connected port is read through the end and two connectors on different ports are distinguishable; the same object path now materializes connect, binding and non-message flow usages with declaration-order ends; message flows and one-ended bindings remain excluded; errors are typed `ErrConnectorEnd` with a source location and all-or-nothing rollback) | +| An untyped or anonymous connector usage materializes on the standard library base of its kind (`interface iface connect a.p to b.q;`, `connect a.p to b.q;`, `bind a.p = b.q;`, `flow a.p to b.q;`), SysML v2 7.13.2, 8.3.13 | `semantics/implicit.go` `implicitUsageBases`; `runtime/connector.go` `connectorBaseOf`, `anonymousConnectors`; `symbols/builder.go` `usageSymbolKind` (bindings use the connection symbol kind) and `semantics/shape.go` `IsShapeFeature` | `conformance/connector_end_identity.sysml`, `conformance/connector_object_binding_flow.sysml`, `connector_test.go` (`TestUntypedConnectorUsageMaterializes`, `TestAnonymousConnectorJoinsItsEnds`, `TestAnonymousConnectorIsMaterializedOnce`, `TestAnonymousBindingAndFlowAreOwnedConnectors`, `TestAnonymousSuccessionIsNoConnector`, `TestEveryConnectorKindAttachesItsEnds`), `parse/connection_implicit_type.golden` | ✅ Faithful (a connection, interface, allocation or connector usage, two-ended binding, or non-message flow that names no definition is an object of its kind's library base with its ends attached, named form and anonymous form alike, and an anonymous one materializes once per object; anonymous successions remain excluded) | +| A flow usage (`flow f from a.out to b.in`) and a binding usage (`binding b bind a.p = b.p`) are connector objects of the kernel layer, but state their ends in their own syntax — `Usage.FlowEnds`, and a binding's two `Usage.ConnectorEnds` — rather than in a `connect` clause | `parser/defusage.go` `parseFlowEnds` and `parseBindingDeclaration`/`parseBindingEnds`; `lower/connection.go` and `lower/binding.go` continue routing and binding semantics; `runtime/connector.go` `materializeConnectorFeatureValue`/`materializeConnectorAs`/`anonymousConnectors`; `semantics/connector.go` `Model.IsConnectorObjectUsage`/`Model.ConnectorObjectEnds` | `semantics/connector_test.go:TestConnectorObjectEnds`, `connector_test.go` (`TestBindingConnectorIsAnObjectOfItsEnds`, `TestFlowConnectorIsAnObjectOfItsEnds`, `TestAnonymousBindingAndFlowAreOwnedConnectors`), `robustness_connector_objects_test.go`, `routing_differential_test.go` | ✅ Faithful (the object model now materializes two-ended bindings and non-message flows through the same connector path as connect usages. `IsConnectorUsage` remains deliberately connect-only for views, document queries and passes; `IsConnectorObjectUsage` and `ConnectorObjectEnds` cover the wider object model. Flow objects do not yet hold their payload; message flows and one-ended bindings are not connector objects.) | | The `of` clause of a binding (`binding b of full = level`) names the feature the binding binds, so it is a reference subsetting rather than a typing (KerML 8.3.3.3.9, SysML v2 8.3.13) | `parser/defusage.go` (`parseBindingEnds` reads what follows `of` as two `ast.ConnectorEnd`s, each attaching the feature it binds; the clause is never a typing) | `parser/binding_of_test.go:TestBindingOfTargetIsAReference`, `parse/constraint_parameterised_conditions.golden` | ✅ Faithful (formerly recorded as a typing, which reported the bound feature as "type must be a definition") | | A declared name wins over an effective one in the same namespace (`part v { perform p; action p; }`) | `symbols/scope.go` `PreferDeclared`, used by `LookupLocal` and `resolve/qualified.go`'s segment walk; `symbols/builder.go` (`Symbol.EffectiveName`) | `semantics/reference_test.go` `TestReferenceFindsSiblingDeclaredAfterIt`, `TestQualifiedNameThroughEffectiveNameIsNotAmbiguous`, `TestRepeatedPerformResolvesToTheAction` | ✅ Faithful | | `individual def X :> PartDef`, `x : IndividualDef` kind compatibility, SysML v2 7.9.4 | `passes/typecheck.go` `occurrenceDefSymbolKinds`/`isOccurrenceDefKind` (specialization) and `isCompatibleTyping` (typing) | `passes/typecheck_individuals_test.go`, corpus gate (`Verification Case Usage Example` now clean) | ✅ Faithful (an `individual def` is an occurrence definition, so it may specialize an occurrence definition of any kind and may type a usage wherever an occurrence definition may; specializing a data type — an attribute or enumeration definition — stays an error per 8.4.5.1, and a usage kind that rejects an occurrence definition, such as a port usage, still rejects an individual definition) | @@ -2402,7 +2402,6 @@ The behavior-execution entries below — interruptible regions, expansion region - Allocation execution - SysML v2 §9.2.4: syntax defined, execution semantics not normative **Implementable But Not Yet Done:** -- Binding and flow connector objects (their ends reach routing, but neither is materialized as a connector object — see the Structural map) - Interruptible regions (spec exists, needs token cancellation) - Exception handlers (spec exists, needs exception propagation) diff --git a/internal/check/passes/typecheck.go b/internal/check/passes/typecheck.go index 058bfaf1b..eba091f74 100644 --- a/internal/check/passes/typecheck.go +++ b/internal/check/passes/typecheck.go @@ -416,7 +416,7 @@ func (tc *typeChecker) checkTypeTarget(scope *symbols.Scope, target ast.Node, re } msg := compatMessage(decl, relKind, kind) if (relKind == ast.RelReferences || relKind == ast.RelSubsets) && - kind == symbols.SymbolUnknown && targetSym.IsFeature() { + targetSym.IsFeature() && (kind == symbols.SymbolUnknown || isBindingUsage(targetSym)) { msg = unclassifiedReferenceKindMessage(decl, relKind, targetSym) } if msg == "" { @@ -480,7 +480,7 @@ func (tc *typeChecker) checkChainReferenceKind(scope *symbols.Scope, target ast. return // unresolved: name-resolution tier owns this } msg := referenceKindMessage(decl, relKind, referentKind(sym)) - if sym.Kind == symbols.SymbolUnknown && sym.IsFeature() { + if sym.IsFeature() && (sym.Kind == symbols.SymbolUnknown || isBindingUsage(sym)) { msg = unclassifiedReferenceKindMessage(decl, relKind, sym) } if msg == "" { @@ -803,6 +803,11 @@ func unclassifiedReferenceKindMessage(decl declKind, rel ast.RelationshipKind, s return referentKindMessage(decl, rel, sym.Kind, sym.Notation()) } +func isBindingUsage(sym *symbols.Symbol) bool { + usage, ok := sym.Decl.(*ast.Usage) + return ok && usage.Kind == ast.UsageBinding +} + func referentKindMessage(decl declKind, rel ast.RelationshipKind, target symbols.SymbolKind, found string) string { if decl.isDef { return "" diff --git a/internal/exec/runtime/adopt.go b/internal/exec/runtime/adopt.go index 8d9829b62..bfffd18e7 100644 --- a/internal/exec/runtime/adopt.go +++ b/internal/exec/runtime/adopt.go @@ -224,7 +224,7 @@ func carriedObject(v Value) (int64, bool) { // connectorFeatureValue reports whether the feature value holds the object of a connector, whose // ends a new context attaches again rather than keeping what they read before. func (ctx *Context) connectorFeatureValue(s *FeatureValue) bool { - return s.Feature != nil && ctx.model.semantics.IsConnectorUsage(s.Feature.Symbol) + return s.Feature != nil && ctx.model.semantics.IsConnectorObjectUsage(s.Feature.Symbol) } // HoldsObject reports whether the value is, or carries, an object of this context: diff --git a/internal/exec/runtime/classifier_behavior.go b/internal/exec/runtime/classifier_behavior.go index 043fcf079..e6cbc294f 100644 --- a/internal/exec/runtime/classifier_behavior.go +++ b/internal/exec/runtime/classifier_behavior.go @@ -508,7 +508,7 @@ func (ctx *Context) behavingParts(typeSym *symbols.Symbol) []int { features := ctx.FeaturesOf(typeSym) parts := []int{} for i := range features { - if !ctx.model.semantics.IsConnectorUsage(features[i].Symbol) && ctx.holdsBehavingPart(&features[i]) { + if !ctx.model.semantics.IsConnectorObjectUsage(features[i].Symbol) && ctx.holdsBehavingPart(&features[i]) { parts = append(parts, i) } } @@ -556,7 +556,7 @@ func (ctx *Context) runsBehaviors(typeSym *symbols.Symbol, visiting map[*symbols if runs { break } - if ctx.model.semantics.IsConnectorUsage(features[i].Symbol) { + if ctx.model.semantics.IsConnectorObjectUsage(features[i].Symbol) { continue } if composite := ctx.requiredPartType(&features[i]); composite != nil && ctx.runsBehaviors(composite, visiting) { diff --git a/internal/exec/runtime/classify_test.go b/internal/exec/runtime/classify_test.go index 86659e310..d5e1e10a0 100644 --- a/internal/exec/runtime/classify_test.go +++ b/internal/exec/runtime/classify_test.go @@ -1695,8 +1695,8 @@ func TestRelationshipsComeFromEveryTypeOfTheObject(t *testing.T) { if strings.Join(conns, ",") != "p-q,q-r" { t.Fatalf("connections = %v, want Base's p-q then Wide's q-r, each once", conns) } - if anon := ctx.anonymousConnectorsOf(raw.types()); len(anon) != 2 { - t.Fatalf("anonymous connectors = %d, want Base's and Wide's", len(anon)) + if anon := ctx.anonymousConnectorsOf(raw.types()); len(anon) != 4 { + t.Fatalf("anonymous connectors = %d, want Base's and Wide's bindings and connections", len(anon)) } } diff --git a/internal/exec/runtime/conformance_test.go b/internal/exec/runtime/conformance_test.go index ffb765814..c6a5aceb5 100644 --- a/internal/exec/runtime/conformance_test.go +++ b/internal/exec/runtime/conformance_test.go @@ -391,6 +391,10 @@ func loadKnownFailures(t *testing.T, conformanceDir string) map[string]bool { // runConformanceCase executes a single conformance test case under policy, or // under the policy the case pins if it pins one. func runConformanceCase(t *testing.T, conformanceDir, caseName string, policy SchedulePolicy) { + runConformanceCaseWithOwned(t, conformanceDir, caseName, policy, false) +} + +func runConformanceCaseWithOwned(t *testing.T, conformanceDir, caseName string, policy SchedulePolicy, forceOwned bool) { // Load .sysml file sysmlPath := filepath.Join(conformanceDir, caseName+".sysml") sysmlData, err := os.ReadFile(sysmlPath) @@ -457,7 +461,7 @@ func runConformanceCase(t *testing.T, conformanceDir, caseName string, policy Sc case "verification": runVerificationConformance(t, ctx, idx, sysmlPath, expected) case "instance": - runInstanceConformance(t, ctx, idx, sysmlPath, expected) + runInstanceConformance(t, ctx, idx, sysmlPath, expected, forceOwned) default: t.Fatalf("unknown test type: %s", expected.Type) } @@ -1477,7 +1481,7 @@ func runSatisfyConformance(t *testing.T, ctx *Context, idx *symbols.Index, path // runInstanceConformance instantiates a type and validates the values its feature values // hold, including derived defaults, plus the verdict of each constraint the // instance carries. -func runInstanceConformance(t *testing.T, ctx *Context, idx *symbols.Index, path string, expected ExpectedOutcome) { +func runInstanceConformance(t *testing.T, ctx *Context, idx *symbols.Index, path string, expected ExpectedOutcome, forceOwned bool) { if expected.Instantiate == "" { t.Fatalf("instance case declares no \"instantiate\" type") } @@ -1495,6 +1499,11 @@ func runInstanceConformance(t *testing.T, ctx *Context, idx *symbols.Index, path if err != nil { t.Fatalf("Instantiate(%s) failed: %v", expected.Instantiate, err) } + if forceOwned { + if _, err := inst.OwnedConnectors(ctx); err != nil { + t.Fatalf("OwnedConnectors(%s) failed before execution: %v", expected.Instantiate, err) + } + } for name, expectedVal := range expected.FeatureValues { // A slot is read as an expression reads it: an optional one holding diff --git a/internal/exec/runtime/connector.go b/internal/exec/runtime/connector.go index e0f7318e3..97ffac945 100644 --- a/internal/exec/runtime/connector.go +++ b/internal/exec/runtime/connector.go @@ -72,7 +72,7 @@ func (ctx *Context) connectorBaseOf(feat *EffectiveFeature) *symbols.Symbol { // they are answered from the ends the usage attaches: `source` and `target` for // a binary connector, `participant` for any other arity. func (ctx *Context) connectorEndFeatures(typeSym *symbols.Symbol, declared map[string]bool) []EffectiveFeature { - ends := ctx.model.semantics.ConnectorEndAttachments(typeSym) + ends := ctx.model.semantics.ConnectorObjectEnds(typeSym) if len(ends) == 0 { return nil } @@ -109,7 +109,7 @@ func (ctx *Context) materializeConnector(owner *Instance, connSym, base *symbols // message, and keep never sees it. Once kept, the older behaviors it woke answer; // one of them failing is reported as its own, with the connector kept. func (ctx *Context) materializeConnectorAs(owner *Instance, connSym, base *symbols.Symbol, id int64, keep func(*Instance)) error { - ends := ctx.model.semantics.ConnectorEndAttachments(connSym) + ends := ctx.model.semantics.ConnectorObjectEnds(connSym) if len(ends) == 0 { return fmt.Errorf("%w: %s declares no end to attach", ErrConnectorEnd, connectorName(connSym)) } @@ -480,13 +480,10 @@ func (ctx *Context) anonymousConnectors(typeSym *symbols.Symbol) []*symbols.Symb if !ok || usage.Ident.Name != "" || usage.Ident.ShortName != "" { continue } - if len(usage.ConnectorEnds) < 2 { - continue - } // A succession or transition carries ends too, and relates its ends in // time rather than joining them, so it is no connector to materialize. sym := anonymousConnectorSymbol(decl, usage) - if !ctx.model.semantics.IsConnectorUsage(sym) { + if len(ctx.model.semantics.ConnectorObjectEnds(sym)) < 2 { continue } out = append(out, sym) @@ -507,6 +504,8 @@ func anonymousConnectorSymbol(typeSym *symbols.Symbol, usage *ast.Usage) *symbol kind = symbols.SymbolInterfaceUsage case ast.UsageAllocation: kind = symbols.SymbolAllocationUsage + case ast.UsageFlow: + kind = symbols.SymbolFlowUsage } return &symbols.Symbol{ Kind: kind, diff --git a/internal/exec/runtime/connector_test.go b/internal/exec/runtime/connector_test.go index 1720ea8ad..2415ebe15 100644 --- a/internal/exec/runtime/connector_test.go +++ b/internal/exec/runtime/connector_test.go @@ -221,6 +221,147 @@ func TestAnonymousConnectorIsMaterializedOnce(t *testing.T) { } } +func TestBindingConnectorIsAnObjectOfItsEnds(t *testing.T) { + inst, ctx := instantiatePart(t, "Sys", ` + package test { + port def P; + part def A { port p : P; } + part def B { port q : P; } + part def Sys { + part a : A; + part b : B; + binding bnd bind a.p = b.q; + } + }`) + port := fvInstance(t, ctx, inst, "a", "p") + peer := fvInstance(t, ctx, inst, "b", "q") + first := fvInstance(t, ctx, inst, "bnd") + second := fvInstance(t, ctx, inst, "bnd") + if len(first.Ends) != 2 { + t.Fatalf("binding has %d ends, want two", len(first.Ends)) + } + if first.Ends[0].Value.Instance != port.ID || first.Ends[1].Value.Instance != peer.ID { + t.Errorf("binding ends = %v, want a.p (%d) and b.q (%d)", first.Ends, port.ID, peer.ID) + } + if first.ID != second.ID { + t.Errorf("binding read as objects %d then %d", first.ID, second.ID) + } +} + +func TestAnonymousBindingAndFlowAreOwnedConnectors(t *testing.T) { + inst, ctx := instantiatePart(t, "Sys", ` + package test { + port def P; + part def A { port p : P; } + part def B { port q : P; } + part def Sys { + part a : A; + part b : B; + bind a.p = b.q; + flow a.p to b.q; + connect a.p to b.q; + } + }`) + port := fvInstance(t, ctx, inst, "a", "p") + peer := fvInstance(t, ctx, inst, "b", "q") + first, err := inst.OwnedConnectors(ctx) + if err != nil { + t.Fatalf("OwnedConnectors: %v", err) + } + second, err := inst.OwnedConnectors(ctx) + if err != nil { + t.Fatalf("OwnedConnectors again: %v", err) + } + if len(first) != 3 || len(second) != 3 { + t.Fatalf("owned connectors = %d then %d, want three", len(first), len(second)) + } + for i, conn := range first { + if len(conn.Ends) != 2 { + t.Errorf("connector %d has %d ends, want two", i, len(conn.Ends)) + } + if len(conn.Ends) == 2 && + (conn.Ends[0].Value.Instance != port.ID || conn.Ends[1].Value.Instance != peer.ID) { + t.Errorf("connector %d ends = %v, want a.p (%d) and b.q (%d)", + i, conn.Ends, port.ID, peer.ID) + } + if conn.ID != second[i].ID { + t.Errorf("connector %d read as %d then %d", i, conn.ID, second[i].ID) + } + } +} + +func TestFlowConnectorIsAnObjectOfItsEnds(t *testing.T) { + inst, ctx := instantiatePart(t, "Sys", ` + package test { + item def Item; + port def P; + part def A { port p : P; } + part def B { port q : P; } + part def Sys { + part a : A; + part b : B; + flow f of Item from a.p to b.q; + } + }`) + port := fvInstance(t, ctx, inst, "a", "p") + peer := fvInstance(t, ctx, inst, "b", "q") + flow := fvInstance(t, ctx, inst, "f") + if len(flow.Ends) != 2 { + t.Fatalf("flow has %d ends, want two", len(flow.Ends)) + } + if got := fvInstance(t, ctx, flow, "source"); got.ID != port.ID { + t.Errorf("flow.source is object %d, want a.p (%d)", got.ID, port.ID) + } + if got := fvInstance(t, ctx, flow, "target"); got.ID != peer.ID { + t.Errorf("flow.target is object %d, want b.q (%d)", got.ID, peer.ID) + } +} + +func TestBindingConnectorEndFollowsAFeatureChain(t *testing.T) { + inst, ctx := instantiatePart(t, "Sys", ` + package test { + port def P; + part def Inner { port p : P; } + part def A { part sub : Inner; } + part def B { port q : P; } + part def Sys { + part a : A; + part b : B; + binding bnd bind a.sub.p = b.q; + } + }`) + port := fvInstance(t, ctx, inst, "a", "sub", "p") + bnd := fvInstance(t, ctx, inst, "bnd") + if got := fvInstance(t, ctx, bnd, "source"); got.ID != port.ID { + t.Errorf("bnd.source is object %d, want a.sub.p (%d)", got.ID, port.ID) + } +} + +func TestBindingConnectorEndsHoldBoundValues(t *testing.T) { + inst, ctx := instantiatePart(t, "Sys", ` + package test { + part def A { attribute x : Integer = 3; } + part def B { attribute y : Integer = 3; } + part def Sys { + part a : A; + part b : B; + bind a.x = b.y; + } + }`) + owned, err := inst.OwnedConnectors(ctx) + if err != nil { + t.Fatalf("OwnedConnectors: %v", err) + } + if len(owned) != 1 || len(owned[0].Ends) != 2 { + t.Fatalf("owned bindings = %v, want one two-ended binding", owned) + } + for i, end := range owned[0].Ends { + if end.Value.Kind != ValConst || end.Value.Const.Int != 3 { + t.Errorf("binding end %d = %s, want 3", i, FormatValue(end.Value)) + } + } +} + const nestedSystem = ` package test { private import ScalarValues::Real; diff --git a/internal/exec/runtime/instance.go b/internal/exec/runtime/instance.go index 07ca2c96d..688c63c50 100644 --- a/internal/exec/runtime/instance.go +++ b/internal/exec/runtime/instance.go @@ -836,13 +836,13 @@ func (inst *Instance) materializeIntrinsic(ctx *Context, fv *FeatureValue, name // An abstract feature has no values of its own (KerML 1.0 §7.3.3.1) and an // optional one demands none: each, a connector included, holds only contributions — // unless the declaration's body binds a feature of the one object it then holds. - if fv.Feature.HoldsOnlyContributions() && !ctx.bodyBindsAFeature(fv.Feature) && (ctx.model.semantics.IsConnectorUsage(fv.Feature.Symbol) || ctx.CompositeTypeOf(fv.Feature) != nil) { + if fv.Feature.HoldsOnlyContributions() && !ctx.bodyBindsAFeature(fv.Feature) && (ctx.model.semantics.IsConnectorObjectUsage(fv.Feature.Symbol) || ctx.CompositeTypeOf(fv.Feature) != nil) { return inst.holdContributions(ctx, fv, name) } // A connector holds the features it connects at its ends rather than objects // of its own, so it is materialized from what the `connect` clause names. - if ctx.model.semantics.IsConnectorUsage(fv.Feature.Symbol) { + if ctx.model.semantics.IsConnectorObjectUsage(fv.Feature.Symbol) { if err := ctx.materializeConnectorFeatureValue(inst, fv, name); err != nil { return nil, err } diff --git a/internal/exec/runtime/robustness_connector_objects_test.go b/internal/exec/runtime/robustness_connector_objects_test.go new file mode 100644 index 000000000..390fd125c --- /dev/null +++ b/internal/exec/runtime/robustness_connector_objects_test.go @@ -0,0 +1,95 @@ +package runtime + +import ( + "errors" + "strings" + "testing" +) + +func TestRuntimeRobustnessConnectorObjects(t *testing.T) { + t.Run("binding_end_names_no_feature", func(t *testing.T) { + inst, ctx := instantiatePart(t, "Sys", ` + package test { + part def Sys { + attribute x : Integer = 1; + binding bnd bind x = missing; + } + }`) + before := len(ctx.instances) + _, err := inst.GetFeatureValue(ctx, "bnd") + if !errors.Is(err, ErrConnectorEnd) { + t.Fatalf("GetFeatureValue(bnd) = %v, want ErrConnectorEnd", err) + } + var endErr *ConnectorEndError + if !errors.As(err, &endErr) { + t.Fatalf("error = %T, want *ConnectorEndError", err) + } + if endErr.Location == "" || !strings.Contains(endErr.Location, "") { + t.Errorf("connector end error location = %q, want location", endErr.Location) + } + if len(ctx.instances) != before { + t.Errorf("failed binding materialized %d object(s)", len(ctx.instances)-before) + } + }) + + t.Run("flow_end_names_no_feature", func(t *testing.T) { + inst, ctx := instantiatePart(t, "Sys", ` + package test { + port def P; + part def Sys { + port p : P; + flow f from p to missing; + } + }`) + before := len(ctx.instances) + _, err := inst.GetFeatureValue(ctx, "f") + if !errors.Is(err, ErrConnectorEnd) { + t.Fatalf("GetFeatureValue(f) = %v, want ErrConnectorEnd", err) + } + var endErr *ConnectorEndError + if !errors.As(err, &endErr) { + t.Fatalf("error = %T, want *ConnectorEndError", err) + } + if endErr.Location == "" || !strings.Contains(endErr.Location, "") { + t.Errorf("connector end error location = %q, want location", endErr.Location) + } + if len(ctx.instances) != before { + t.Errorf("failed flow materialized %d object(s)", len(ctx.instances)-before) + } + }) + + t.Run("binding_with_no_valued_end_as_object", func(t *testing.T) { + inst, ctx := instantiatePart(t, "Sys", ` + package test { + part def Sys { + attribute x : Integer; + attribute y : Integer; + binding bnd bind x = y; + } + }`) + _, err := inst.GetFeatureValue(ctx, "bnd") + if !errors.Is(err, ErrConnectorEnd) && !errors.Is(err, ErrBindingEnd) { + t.Fatalf("GetFeatureValue(bnd) = %v, want connector or binding end error", err) + } + }) + + t.Run("one_ended_binding_is_no_connector_object", func(t *testing.T) { + inst, ctx := instantiatePart(t, "Sys", ` + package test { + part def Sys { + attribute x : Integer = 1; + binding b of x; + } + }`) + owned, err := inst.OwnedConnectors(ctx) + if err != nil { + t.Fatalf("OwnedConnectors: %v", err) + } + if len(owned) != 0 { + t.Fatalf("owned connectors = %d, want none", len(owned)) + } + if _, err := inst.GetFeatureValue(ctx, "b"); err != nil && errors.Is(err, ErrConnectorEnd) { + t.Fatalf("one-ended binding read as connector: %v", err) + } + }) +} diff --git a/internal/exec/runtime/routing_differential_test.go b/internal/exec/runtime/routing_differential_test.go new file mode 100644 index 000000000..dffe73d4f --- /dev/null +++ b/internal/exec/runtime/routing_differential_test.go @@ -0,0 +1,57 @@ +package runtime + +import ( + "encoding/json" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "testing" +) + +func TestRoutingUnchangedByConnectorObjects(t *testing.T) { + const conformanceDir = "testdata/conformance" + pattern := regexp.MustCompile(`^(send_|port_|.*_routing|action_port_communication|w7d_send_via_port_to_receiver|binding_)`) + entries, err := os.ReadDir(conformanceDir) + if err != nil { + t.Fatal(err) + } + var cases []string + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".expected.json") { + continue + } + name := strings.TrimSuffix(entry.Name(), ".expected.json") + if pattern.MatchString(name) { + if _, err := os.Stat(filepath.Join(conformanceDir, name+".sysml")); err != nil { + continue + } + cases = append(cases, name) + } + } + sort.Strings(cases) + known := loadKnownFailures(t, conformanceDir) + for _, name := range cases { + name := name + t.Run(name, func(t *testing.T) { + if known[name] { + t.Skip("known conformance failure") + } + data, err := os.ReadFile(filepath.Join(conformanceDir, name+".expected.json")) + if err != nil { + t.Fatal(err) + } + var expected ExpectedOutcome + if err := json.Unmarshal(data, &expected); err != nil { + t.Fatal(err) + } + runConformanceCase(t, conformanceDir, name, DefaultSchedulePolicy) + if expected.Type != "instance" { + t.Log("no root instance exists before behavior execution; normal execution is the only applicable routing comparison") + return + } + runConformanceCaseWithOwned(t, conformanceDir, name, DefaultSchedulePolicy, true) + }) + } +} diff --git a/internal/exec/runtime/subsetting.go b/internal/exec/runtime/subsetting.go index 3cf150bd8..06205fc19 100644 --- a/internal/exec/runtime/subsetting.go +++ b/internal/exec/runtime/subsetting.go @@ -443,7 +443,7 @@ func fewestOf(mult semantics.Range) int64 { func (ctx *Context) fillsFromSubsetted(feat *EffectiveFeature) bool { lower := feat.Multiplicity.Lower return lower.Known && !lower.Infinite && lower.Value == 0 && - !ctx.model.semantics.IsConnectorUsage(feat.Symbol) && ctx.CompositeTypeOf(feat) != nil + !ctx.model.semantics.IsConnectorObjectUsage(feat.Symbol) && ctx.CompositeTypeOf(feat) != nil } // materializeSubsettedCollections reads the collections an optional feature subsets before the diff --git a/internal/exec/runtime/testdata/conformance/connector_object_binding_flow.expected.json b/internal/exec/runtime/testdata/conformance/connector_object_binding_flow.expected.json new file mode 100644 index 000000000..39d3d0f21 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/connector_object_binding_flow.expected.json @@ -0,0 +1,14 @@ +{ + "type": "instance", + "instantiate": "test::Sys", + "slots": { + "bnd": {"type": "Instance"}, + "f": {"type": "Instance"} + }, + "identical": [ + ["bnd.source", "a.p"], + ["bnd.target", "b.q"], + ["f.source", "a.p"], + ["f.target", "b.q"] + ] +} diff --git a/internal/exec/runtime/testdata/conformance/connector_object_binding_flow.sysml b/internal/exec/runtime/testdata/conformance/connector_object_binding_flow.sysml new file mode 100644 index 000000000..9ef758d2c --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/connector_object_binding_flow.sysml @@ -0,0 +1,14 @@ +package test { + port def P; + part def A { port p : P; } + part def B { port q : P; } + + part def Sys { + part a : A; + part b : B; + binding bnd bind a.p = b.q; + flow f from a.p to b.q; + } + + part sys : Sys; +} diff --git a/internal/exec/runtime/variation.go b/internal/exec/runtime/variation.go index beb646ca5..eaa6d85c1 100644 --- a/internal/exec/runtime/variation.go +++ b/internal/exec/runtime/variation.go @@ -170,7 +170,7 @@ func (ctx *Context) variantValue(variation, variant *symbols.Symbol, owner int64 // selected it, with its ends attached to that object's features. A variant of // any other kind is an ordinary object of itself. keep receives the object once created. func (ctx *Context) variantInstance(variant *symbols.Symbol, owner int64, keep func(*Instance)) error { - if !ctx.model.semantics.IsConnectorUsage(variant) { + if !ctx.model.semantics.IsConnectorObjectUsage(variant) { inst, err := ctx.instantiateAs(variant, 0) if err != nil { return err diff --git a/internal/frontend/repl/complete.go b/internal/frontend/repl/complete.go index 146680d85..754b785bf 100644 --- a/internal/frontend/repl/complete.go +++ b/internal/frontend/repl/complete.go @@ -473,7 +473,7 @@ func (s *Session) objectTypeOf(feat *runtime.EffectiveFeature) *symbols.Symbol { if typ := s.rtCtx.CompositeTypeOf(feat); typ != nil { return typ } - if s.rtCtx.Semantics().IsConnectorUsage(feat.Symbol) { + if s.rtCtx.Semantics().IsConnectorObjectUsage(feat.Symbol) { return feat.Symbol } return nil diff --git a/internal/semantic/semantics/connector.go b/internal/semantic/semantics/connector.go index 666158f0c..8137260e5 100644 --- a/internal/semantic/semantics/connector.go +++ b/internal/semantic/semantics/connector.go @@ -565,6 +565,29 @@ func (m *Model) IsConnectorUsage(sym *symbols.Symbol) bool { return false } +// IsConnectorObjectUsage reports whether sym is materialized as a connector +// object from the features its ends name. +func (m *Model) IsConnectorObjectUsage(sym *symbols.Symbol) bool { + if m.IsConnectorUsage(sym) { + return true + } + if sym == nil || !m.isConnectorLike(sym) { + return false + } + usage, ok := sym.Decl.(*ast.Usage) + if !ok { + return false + } + switch usage.Kind { + case ast.UsageBinding: + return len(usage.ConnectorEnds) == 2 + case ast.UsageFlow: + return usage.Keyword != "message" && usage.FlowEnds != nil && + usage.FlowEnds.From != nil && usage.FlowEnds.To != nil + } + return false +} + // ConnectorEndAttachment is one end of a connector usage as an object of that // usage carries it: the name of the end feature the position occupies, and the // node naming the feature the end attaches to. A connector end @@ -619,21 +642,57 @@ func (m *Model) ConnectorEndAttachments(sym *symbols.Symbol) []ConnectorEndAttac return nil } usage := sym.Decl.(*ast.Usage) - owned := ownedEnds(sym) + ends := make([]connectorEndInput, 0, len(usage.ConnectorEnds)) + for _, end := range usage.ConnectorEnds { + if end != nil { + ends = append(ends, connectorEndInput{attachment: end.AttachedTarget(), end: end}) + } + } + return m.connectorEndAttachments(sym, ends) +} - out := make([]ConnectorEndAttachment, 0, len(usage.ConnectorEnds)) - for i, end := range usage.ConnectorEnds { - if end == nil { - continue +// ConnectorObjectEnds returns the ends of any connector object usage in +// declaration order: a connect clause, a binding, or a flow's from/to ends. +func (m *Model) ConnectorObjectEnds(sym *symbols.Symbol) []ConnectorEndAttachment { + if !m.IsConnectorObjectUsage(sym) { + return nil + } + usage := sym.Decl.(*ast.Usage) + var ends []connectorEndInput + switch { + case m.IsConnectorUsage(sym), usage.Kind == ast.UsageBinding: + ends = make([]connectorEndInput, 0, len(usage.ConnectorEnds)) + for _, end := range usage.ConnectorEnds { + if end != nil { + ends = append(ends, connectorEndInput{attachment: end.AttachedTarget(), end: end}) + } } - att := ConnectorEndAttachment{Attachment: end.AttachedTarget(), End: end} + case usage.Kind == ast.UsageFlow: + ends = []connectorEndInput{ + {attachment: usage.FlowEnds.From}, + {attachment: usage.FlowEnds.To}, + } + } + return m.connectorEndAttachments(sym, ends) +} + +type connectorEndInput struct { + attachment ast.Node + end *ast.ConnectorEnd +} + +func (m *Model) connectorEndAttachments(sym *symbols.Symbol, ends []connectorEndInput) []ConnectorEndAttachment { + owned := ownedEnds(sym) + out := make([]ConnectorEndAttachment, 0, len(ends)) + for i, end := range ends { + att := ConnectorEndAttachment{Attachment: end.attachment, End: end.end} general := m.generalEndAt(sym, i) switch { case i < len(owned) && owned[i] != nil: att.Name, att.EndFeature = leafName(owned[i].Name), owned[i] case general != nil: att.Name, att.EndFeature = leafName(general.Name), general - case len(usage.ConnectorEnds) == 2: + case len(ends) == 2: att.Name = binaryConnectorEndNames[i] } out = append(out, att) diff --git a/internal/semantic/semantics/connector_test.go b/internal/semantic/semantics/connector_test.go index d9b0ce1b2..e20dfc179 100644 --- a/internal/semantic/semantics/connector_test.go +++ b/internal/semantic/semantics/connector_test.go @@ -5,6 +5,7 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" "github.com/Open-MBEE/OpenSysML/internal/syntax/parser" "github.com/Open-MBEE/OpenSysML/internal/syntax/source" ) @@ -415,6 +416,89 @@ func TestConnectorEndAttachments(t *testing.T) { } } +func TestConnectorObjectEnds(t *testing.T) { + m, root := buildModel(t, `package P { + part def A { attribute x : Integer = 3; } + part def B { attribute y : Integer = 4; } + part w { + part a : A; + part b : B; + binding named bind left ::> a = right ::> b; + bind a = b; + flow of Integer from a.x to b.y; + flow flowNamed of Integer from a.x to b.y; + flow a.x to b.y; + message msg of Integer from a.x to b.y; + succession first a then b; + } + }`) + w := nested(t, sym(t, root, "P").Scope, "w") + for _, name := range []string{"named"} { + sym := nested(t, w.Scope, name) + if !m.IsConnectorObjectUsage(sym) { + t.Fatalf("IsConnectorObjectUsage(%s) = false", name) + } + got := m.ConnectorObjectEnds(sym) + if len(got) != 2 || got[0].Attachment == nil || got[1].Attachment == nil { + t.Fatalf("%s ends = %+v, want two attached ends", name, got) + } + if got[0].Name != "left" || got[1].Name != "right" || + ast.QualifiedText(got[0].Attachment) != "a" || + ast.QualifiedText(got[1].Attachment) != "b" { + t.Fatalf("%s ends = %+v, want left/right attached to a/b", name, got) + } + if m.ConnectorEndAttachments(sym) != nil { + t.Fatalf("ConnectorEndAttachments(%s) should remain connect-only", name) + } + } + for _, name := range []string{"flowNamed"} { + sym := nested(t, w.Scope, name) + if !m.IsConnectorObjectUsage(sym) { + t.Errorf("IsConnectorObjectUsage(%s) = false", name) + } + got := m.ConnectorObjectEnds(sym) + if len(got) != 2 || got[0].Name != "source" || got[1].Name != "target" || + got[0].Attachment == nil || got[1].Attachment == nil { + t.Errorf("%s ends = %+v, want source/target attached to a.x/b.y", name, got) + } + if m.ConnectorEndAttachments(sym) != nil { + t.Fatalf("ConnectorEndAttachments(%s) should remain connect-only", name) + } + } + var anonymousBindings, anonymousFlows, successions int + for _, sym := range w.Scope.AnonymousMembers() { + usage, ok := sym.Decl.(*ast.Usage) + if !ok { + continue + } + switch usage.Kind { + case ast.UsageBinding: + anonymousBindings++ + if !m.IsConnectorObjectUsage(sym) { + t.Error("anonymous binding is not a connector object") + } + case ast.UsageFlow: + anonymousFlows++ + if usage.Keyword == "message" { + if m.IsConnectorObjectUsage(sym) { + t.Error("message flow is a connector object") + } + } else if !m.IsConnectorObjectUsage(sym) { + t.Error("anonymous flow is not a connector object") + } + case ast.UsageSuccession: + successions++ + if m.IsConnectorObjectUsage(sym) { + t.Error("succession is a connector object") + } + } + } + if anonymousBindings != 1 || anonymousFlows != 2 || successions != 1 { + t.Fatalf("anonymous bindings=%d flows=%d successions=%d, want 1, 2, 1", + anonymousBindings, anonymousFlows, successions) + } +} + // TestEndsInheritedThroughSeveralGenerals covers a connector that specializes // two connectors: its ends redefine the end at their position in each general, // and an inherited end that another inherited end redefines counts once. diff --git a/internal/semantic/semantics/shape.go b/internal/semantic/semantics/shape.go index a55b12566..5dba50da4 100644 --- a/internal/semantic/semantics/shape.go +++ b/internal/semantic/semantics/shape.go @@ -208,6 +208,10 @@ func IsShapeFeature(sym *symbols.Symbol) bool { if sym == nil { return false } + if sym.Kind == symbols.SymbolUnknown { + usage, ok := sym.Decl.(*ast.Usage) + return ok && usage.Kind == ast.UsageBinding + } switch sym.Kind { case symbols.SymbolAttributeUsage, symbols.SymbolPartUsage, symbols.SymbolItemUsage, symbols.SymbolPortUsage, symbols.SymbolConnectionUsage, symbols.SymbolActionUsage, From 5db93e98e24885727a3bb5010c26fa0945adf7c1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 19:10:37 +0000 Subject: [PATCH 2/3] fix(symbols): classify binding usages as their own symbol kind Co-Authored-By: jason.han --- docs/project/spec-compliance.md | 2 +- internal/check/passes/typecheck.go | 12 ++--- .../passes/typecheck_assert_reference_test.go | 20 +++---- internal/exec/runtime/conformance_test.go | 50 ++++++++++++++++-- internal/exec/runtime/connector.go | 2 + .../exec/runtime/routing_differential_test.go | 18 ++++++- internal/semantic/query/metamodel.go | 1 + internal/semantic/semantics/annotations.go | 1 + internal/semantic/semantics/filter.go | 2 +- internal/semantic/semantics/shape.go | 6 +-- internal/semantic/symbols/builder.go | 2 + .../semantic/symbols/kind_mapping_test.go | 2 +- internal/semantic/symbols/symbol.go | 6 ++- internal/workspace/libs/record.go | 2 +- internal/workspace/libs/snapshot.go | 2 +- internal/workspace/libs/snapshot_test.go | 2 +- internal/workspace/libs/stdlib.snapshot | Bin 3688657 -> 3688674 bytes 17 files changed, 95 insertions(+), 35 deletions(-) diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 4c5dc8bfc..6b8742c15 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -1922,7 +1922,7 @@ semantics layer over the conjugation parity of the typing/specialization chain. | N-ary connector ends (`connection link connect (a, b, c)`), SysML v2 7.13.2, 8.3.13 | `parser/defusage.go` `parseConnectorEnds` (parenthesized end list, reached by both the named declaration and the anonymous `connect …;` body member) over `parseConnectorEnd`, which takes exactly one target per end and only the relationship forms the `ConnectorEnd` production allows (`::>`, `references`, an explicit `:>>`), so a comma separates ends instead of being read as a further relationship of the first one; `passes/constraint.go` `checkConnectorEnds` (arity by kind, the interface case gated on `semantics.Model.IsBinaryConnector`, since `Interfaces::Interface` is n-ary — `ref port :>> participant : Port[2..*]` — and only `BinaryInterface` narrows it to two); `semantics/implicit.go` gives exactly two-ended untyped interface/connection definitions and usages their binary bases; `lower/connection.go` `lowerConnections`, `PeerPorts` | `parse/connection_nary.golden`, `parser/connector_ends_nary_test.go` `TestParseNaryConnectorEndsKeepsEveryEnd`, `parser/negative_test.go` (`nary_connect_unclosed`, `nary_connect_trailing_comma`, `nary_connect_empty`), `passes/constraint_test.go` `TestConstraintConnectionNaryEndCountReachesTheChecker`, `passes/w9c_rules_test.go` `TestW9CBinaryInterfaceEndDiamondWarns`, `TestW9CNonBinaryConnectorEndsStaySilent`, `TestConstraintBinaryInterfaceNaryFails`, `lower/connection_test.go` `TestLowerNaryConnectionKeepsEveryEnd` and `TestLowerAnonymousNaryConnectionKeepsEveryEnd`, `parser/connector_ends_nary_test.go` `TestParseAnonymousInlineConnectKeepsEveryEnd`, conformance `action_port_communication_nary.sysml` and `action_port_communication_nary_anonymous.sysml` | ✅ Faithful: SysML v2 §7.14.1 permits three or more ends on a general interface; §7.14.2 and §8.3.14.2 constrain `BinaryInterface`. Exactly two-ended untyped interfaces and connections receive binary typing, an explicit binary interface with too many ends errors, and a three-ended general interface remains general. The universal `InterfaceUsage_Invalid.sysml.xt:49` expectation is a pilot limitation | | Anonymous binary allocation (`allocate torqueGenerator to powerTrain`) | `parser/defusage.go` `atAllocateShorthand` | `parse/perform_reference.golden` | ✅ Faithful (both names are connector ends; formerly the first was read as the usage's name) | | An object of a connector usage holds the features it connects at its ends (`connection link : Link connect a.p to b.q` makes `link.source` **be** `a.p`), KerML 7.4.6, SysML v2 7.13.2 | `runtime/connector.go` `materializeConnectorFeatureValue`, `materializeConnector`, `attachConnectorEnd`, `bindEndFeatureValue`, `bindParticipants`, reached from `runtime/instance.go` `GetFeatureValue`; end features synthesized by `runtime/shape.go` `connectorEndFeatures`; attachments and effective end names by `semantics/connector.go` `Model.ConnectorObjectEnds`, `Model.IsConnectorObjectUsage`; inherited ends aliased by `runtime/subsetting.go` over `Model.ImplicitEndRedefinitions` | `connector_test.go` (`TestConnectorEndsAreTheConnectedFeatures`, `TestWritingAConnectedPortIsReadThroughTheEnd`, `TestConnectorEndFollowsAFeatureChain`, `TestConnectorEndAttachesToAPart`, `TestNaryConnectorKeepsEveryEnd`, `TestRedefinedEndSharesTheInheritedFeatureValue`, `TestEveryConnectorKindAttachesItsEnds`, `TestBindingConnectorIsAnObjectOfItsEnds`, `TestFlowConnectorIsAnObjectOfItsEnds`, `TestBindingConnectorEndFollowsAFeatureChain`, `TestBindingConnectorEndsHoldBoundValues`), `conformance/connector_end_identity.sysml`, `conformance/connector_object_binding_flow.sysml`, `semantics/connector_test.go:TestConnectorEndAttachments`, `semantics/connector_test.go:TestConnectorObjectEnds`, `robustness_connector_objects_test.go`, `robustness_test.go:unattachable_connector_end`, `multiplicity_on_a_connector`, `connector_attached_to_itself`, `mutually_attached_connectors` | ✅ Faithful (an end holds the very object the connector attaches to, so writing the connected port is read through the end and two connectors on different ports are distinguishable; the same object path now materializes connect, binding and non-message flow usages with declaration-order ends; message flows and one-ended bindings remain excluded; errors are typed `ErrConnectorEnd` with a source location and all-or-nothing rollback) | -| An untyped or anonymous connector usage materializes on the standard library base of its kind (`interface iface connect a.p to b.q;`, `connect a.p to b.q;`, `bind a.p = b.q;`, `flow a.p to b.q;`), SysML v2 7.13.2, 8.3.13 | `semantics/implicit.go` `implicitUsageBases`; `runtime/connector.go` `connectorBaseOf`, `anonymousConnectors`; `symbols/builder.go` `usageSymbolKind` (bindings use the connection symbol kind) and `semantics/shape.go` `IsShapeFeature` | `conformance/connector_end_identity.sysml`, `conformance/connector_object_binding_flow.sysml`, `connector_test.go` (`TestUntypedConnectorUsageMaterializes`, `TestAnonymousConnectorJoinsItsEnds`, `TestAnonymousConnectorIsMaterializedOnce`, `TestAnonymousBindingAndFlowAreOwnedConnectors`, `TestAnonymousSuccessionIsNoConnector`, `TestEveryConnectorKindAttachesItsEnds`), `parse/connection_implicit_type.golden` | ✅ Faithful (a connection, interface, allocation or connector usage, two-ended binding, or non-message flow that names no definition is an object of its kind's library base with its ends attached, named form and anonymous form alike, and an anonymous one materializes once per object; anonymous successions remain excluded) | +| An untyped or anonymous connector usage materializes on the standard library base of its kind (`interface iface connect a.p to b.q;`, `connect a.p to b.q;`, `bind a.p = b.q;`, `flow a.p to b.q;`), SysML v2 7.13.2, 8.3.13 | `semantics/implicit.go` `implicitUsageBases`; `runtime/connector.go` `connectorBaseOf`, `anonymousConnectors`; `symbols/builder.go` `usageSymbolKind` (bindings use `SymbolBindingUsage`) and `semantics/shape.go` `IsShapeFeature` | `conformance/connector_end_identity.sysml`, `conformance/connector_object_binding_flow.sysml`, `connector_test.go` (`TestUntypedConnectorUsageMaterializes`, `TestAnonymousConnectorJoinsItsEnds`, `TestAnonymousConnectorIsMaterializedOnce`, `TestAnonymousBindingAndFlowAreOwnedConnectors`, `TestAnonymousSuccessionIsNoConnector`, `TestEveryConnectorKindAttachesItsEnds`), `parse/connection_implicit_type.golden` | ✅ Faithful (a connection, interface, allocation or connector usage, two-ended binding, or non-message flow that names no definition is an object of its kind's library base with its ends attached, named form and anonymous form alike, and an anonymous one materializes once per object; anonymous successions remain excluded) | | A flow usage (`flow f from a.out to b.in`) and a binding usage (`binding b bind a.p = b.p`) are connector objects of the kernel layer, but state their ends in their own syntax — `Usage.FlowEnds`, and a binding's two `Usage.ConnectorEnds` — rather than in a `connect` clause | `parser/defusage.go` `parseFlowEnds` and `parseBindingDeclaration`/`parseBindingEnds`; `lower/connection.go` and `lower/binding.go` continue routing and binding semantics; `runtime/connector.go` `materializeConnectorFeatureValue`/`materializeConnectorAs`/`anonymousConnectors`; `semantics/connector.go` `Model.IsConnectorObjectUsage`/`Model.ConnectorObjectEnds` | `semantics/connector_test.go:TestConnectorObjectEnds`, `connector_test.go` (`TestBindingConnectorIsAnObjectOfItsEnds`, `TestFlowConnectorIsAnObjectOfItsEnds`, `TestAnonymousBindingAndFlowAreOwnedConnectors`), `robustness_connector_objects_test.go`, `routing_differential_test.go` | ✅ Faithful (the object model now materializes two-ended bindings and non-message flows through the same connector path as connect usages. `IsConnectorUsage` remains deliberately connect-only for views, document queries and passes; `IsConnectorObjectUsage` and `ConnectorObjectEnds` cover the wider object model. Flow objects do not yet hold their payload; message flows and one-ended bindings are not connector objects.) | | The `of` clause of a binding (`binding b of full = level`) names the feature the binding binds, so it is a reference subsetting rather than a typing (KerML 8.3.3.3.9, SysML v2 8.3.13) | `parser/defusage.go` (`parseBindingEnds` reads what follows `of` as two `ast.ConnectorEnd`s, each attaching the feature it binds; the clause is never a typing) | `parser/binding_of_test.go:TestBindingOfTargetIsAReference`, `parse/constraint_parameterised_conditions.golden` | ✅ Faithful (formerly recorded as a typing, which reported the bound feature as "type must be a definition") | | A declared name wins over an effective one in the same namespace (`part v { perform p; action p; }`) | `symbols/scope.go` `PreferDeclared`, used by `LookupLocal` and `resolve/qualified.go`'s segment walk; `symbols/builder.go` (`Symbol.EffectiveName`) | `semantics/reference_test.go` `TestReferenceFindsSiblingDeclaredAfterIt`, `TestQualifiedNameThroughEffectiveNameIsNotAmbiguous`, `TestRepeatedPerformResolvesToTheAction` | ✅ Faithful | diff --git a/internal/check/passes/typecheck.go b/internal/check/passes/typecheck.go index eba091f74..c1caa9213 100644 --- a/internal/check/passes/typecheck.go +++ b/internal/check/passes/typecheck.go @@ -416,7 +416,7 @@ func (tc *typeChecker) checkTypeTarget(scope *symbols.Scope, target ast.Node, re } msg := compatMessage(decl, relKind, kind) if (relKind == ast.RelReferences || relKind == ast.RelSubsets) && - targetSym.IsFeature() && (kind == symbols.SymbolUnknown || isBindingUsage(targetSym)) { + kind == symbols.SymbolUnknown && targetSym.IsFeature() { msg = unclassifiedReferenceKindMessage(decl, relKind, targetSym) } if msg == "" { @@ -480,7 +480,7 @@ func (tc *typeChecker) checkChainReferenceKind(scope *symbols.Scope, target ast. return // unresolved: name-resolution tier owns this } msg := referenceKindMessage(decl, relKind, referentKind(sym)) - if sym.IsFeature() && (sym.Kind == symbols.SymbolUnknown || isBindingUsage(sym)) { + if sym.Kind == symbols.SymbolUnknown && sym.IsFeature() { msg = unclassifiedReferenceKindMessage(decl, relKind, sym) } if msg == "" { @@ -798,16 +798,11 @@ func referentKind(sym *symbols.Symbol) symbols.SymbolKind { } // unclassifiedReferenceKindMessage judges a referent the builder leaves without a -// kind (a named binding): a feature of no constraint kind, named by its notation. +// kind: a feature of no constraint kind, named by its notation. func unclassifiedReferenceKindMessage(decl declKind, rel ast.RelationshipKind, sym *symbols.Symbol) string { return referentKindMessage(decl, rel, sym.Kind, sym.Notation()) } -func isBindingUsage(sym *symbols.Symbol) bool { - usage, ok := sym.Decl.(*ast.Usage) - return ok && usage.Kind == ast.UsageBinding -} - func referentKindMessage(decl declKind, rel ast.RelationshipKind, target symbols.SymbolKind, found string) string { if decl.isDef { return "" @@ -994,6 +989,7 @@ var usageSymbolKinds = map[symbols.SymbolKind]bool{ symbols.SymbolRenderingUsage: true, symbols.SymbolConcernUsage: true, symbols.SymbolConnectionUsage: true, + symbols.SymbolBindingUsage: true, symbols.SymbolSuccessionUsage: true, symbols.SymbolFlowUsage: true, symbols.SymbolPortUsage: true, diff --git a/internal/check/passes/typecheck_assert_reference_test.go b/internal/check/passes/typecheck_assert_reference_test.go index 051d8c766..99cb9a10e 100644 --- a/internal/check/passes/typecheck_assert_reference_test.go +++ b/internal/check/passes/typecheck_assert_reference_test.go @@ -98,8 +98,8 @@ part ctx { ` } } -// A named binding is a feature the builder leaves unclassified, and a named -// transition is an action usage; neither is a constraint or a requirement. +// A named binding is a binding usage, and a named transition is an action usage; +// neither is a constraint or a requirement. func TestAssertReferenceToBindingOrTransitionRejected(t *testing.T) { prefix := `part def Holder { attribute x; attribute y; binding b bind x = y; alias ab for b; @@ -111,15 +111,15 @@ part ctx { ` tests := []struct { name, target, want string }{ - {"binding", "assert b;", "assert target must be a constraint usage, found binding"}, - {"negated binding", "assert not b;", "assert target must be a constraint usage, found binding"}, - {"alias to binding", "assert ab;", "assert target must be a constraint usage, found binding"}, - {"chained binding", "assert h.b;", "assert target must be a constraint usage, found binding"}, - {"chained alias to binding", "assert h.ab;", "assert target must be a constraint usage, found binding"}, + {"binding", "assert b;", "assert target must be a constraint usage, found bindingUsage"}, + {"negated binding", "assert not b;", "assert target must be a constraint usage, found bindingUsage"}, + {"alias to binding", "assert ab;", "assert target must be a constraint usage, found bindingUsage"}, + {"chained binding", "assert h.b;", "assert target must be a constraint usage, found bindingUsage"}, + {"chained alias to binding", "assert h.ab;", "assert target must be a constraint usage, found bindingUsage"}, {"chained transition", "assert h.sm.t;", "assert target must be a constraint usage, found actionUsage"}, - {"binding in constraint body", "} constraint def K { assert b;", "assert target must be a constraint usage, found binding"}, - {"satisfy binding", "satisfy b;", "satisfy target must be a requirement usage, found binding"}, - {"satisfy chained binding", "satisfy h.b;", "satisfy target must be a requirement usage, found binding"}, + {"binding in constraint body", "} constraint def K { assert b;", "assert target must be a constraint usage, found bindingUsage"}, + {"satisfy binding", "satisfy b;", "satisfy target must be a requirement usage, found bindingUsage"}, + {"satisfy chained binding", "satisfy h.b;", "satisfy target must be a requirement usage, found bindingUsage"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/exec/runtime/conformance_test.go b/internal/exec/runtime/conformance_test.go index c6a5aceb5..406198cb7 100644 --- a/internal/exec/runtime/conformance_test.go +++ b/internal/exec/runtime/conformance_test.go @@ -443,9 +443,12 @@ func runConformanceCaseWithOwned(t *testing.T, conformanceDir, caseName string, // Dispatch based on type switch expected.Type { case "action": + if forceOwned { + t.Skip("no object owns connectors before execution") + } runActionConformance(t, ctx, idx, sysmlPath, expected) case "state": - runStateConformance(t, ctx, idx, sysmlPath, expected) + runStateConformance(t, ctx, idx, sysmlPath, expected, forceOwned) case "calc": runCalcConformance(t, ctx, idx, sysmlPath, expected) case "calcUsage": @@ -459,10 +462,16 @@ func runConformanceCaseWithOwned(t *testing.T, conformanceDir, caseName string, case "analysis": runAnalysisConformance(t, ctx, idx, sysmlPath, expected) case "verification": + if forceOwned { + t.Skip("no object owns connectors before execution") + } runVerificationConformance(t, ctx, idx, sysmlPath, expected) case "instance": runInstanceConformance(t, ctx, idx, sysmlPath, expected, forceOwned) default: + if forceOwned { + t.Skip("no object owns connectors before execution") + } t.Fatalf("unknown test type: %s", expected.Type) } // Exploration is the same under any policy, so the default suite does it once. @@ -912,10 +921,13 @@ func runActionConformance(t *testing.T, ctx *Context, idx *symbols.Index, path s // runStateConformance executes a state machine and validates the final state. A // case naming performers runs the machine once per object performing it, each // against the outcome that object expects. -func runStateConformance(t *testing.T, ctx *Context, idx *symbols.Index, path string, expected ExpectedOutcome) { +func runStateConformance(t *testing.T, ctx *Context, idx *symbols.Index, path string, expected ExpectedOutcome, forceOwned bool) { rootScope := idx.DocumentRoot(path) stateSym := namedOrFoundSymbol(t, idx, expected.Evaluate, rootScope, ast.DefState, ast.UsageState) if len(expected.Performers) == 0 { + if forceOwned { + t.Skip("no object owns connectors before execution") + } runOneStatePerformance(t, ctx, stateSym, nil, expected) return } @@ -924,6 +936,11 @@ func runStateConformance(t *testing.T, ctx *Context, idx *symbols.Index, path st if err != nil { t.Fatalf("instantiate %s: %v", performer.Object, err) } + if forceOwned { + if err := forceOwnedConnectors(ctx, self); err != nil { + t.Fatalf("OwnedConnectors(%s) failed before execution: %v", performer.Object, err) + } + } t.Run(performer.Object, func(t *testing.T) { runOneStatePerformance(t, ctx, stateSym, self, ExpectedOutcome{ Events: performer.Events, @@ -936,6 +953,33 @@ func runStateConformance(t *testing.T, ctx *Context, idx *symbols.Index, path st } } +func forceOwnedConnectors(ctx *Context, root *Instance) error { + visited := make(map[int64]bool) + var walk func(*Instance) error + walk = func(inst *Instance) error { + if inst == nil || visited[inst.ID] { + return nil + } + visited[inst.ID] = true + if _, err := inst.OwnedConnectors(ctx); err != nil { + return err + } + for _, feature := range ctx.FeaturesOfObject(inst) { + fv, err := inst.GetFeatureValue(ctx, feature.Name) + if err != nil { + continue + } + for _, child := range heldInstances(ctx, fv) { + if err := walk(child); err != nil { + return err + } + } + } + return nil + } + return walk(root) +} + // queuedEvents converts the events a case declares into the events the runtime // queues, so the conformance, trace and snapshot harnesses drive one performance. func queuedEvents(t *testing.T, events []ExpectedEvent) []QueuedEvent { @@ -1500,7 +1544,7 @@ func runInstanceConformance(t *testing.T, ctx *Context, idx *symbols.Index, path t.Fatalf("Instantiate(%s) failed: %v", expected.Instantiate, err) } if forceOwned { - if _, err := inst.OwnedConnectors(ctx); err != nil { + if err := forceOwnedConnectors(ctx, inst); err != nil { t.Fatalf("OwnedConnectors(%s) failed before execution: %v", expected.Instantiate, err) } } diff --git a/internal/exec/runtime/connector.go b/internal/exec/runtime/connector.go index 97ffac945..ef0d0b9f5 100644 --- a/internal/exec/runtime/connector.go +++ b/internal/exec/runtime/connector.go @@ -506,6 +506,8 @@ func anonymousConnectorSymbol(typeSym *symbols.Symbol, usage *ast.Usage) *symbol kind = symbols.SymbolAllocationUsage case ast.UsageFlow: kind = symbols.SymbolFlowUsage + case ast.UsageBinding: + kind = symbols.SymbolBindingUsage } return &symbols.Symbol{ Kind: kind, diff --git a/internal/exec/runtime/routing_differential_test.go b/internal/exec/runtime/routing_differential_test.go index dffe73d4f..e199b7ef8 100644 --- a/internal/exec/runtime/routing_differential_test.go +++ b/internal/exec/runtime/routing_differential_test.go @@ -23,7 +23,7 @@ func TestRoutingUnchangedByConnectorObjects(t *testing.T) { continue } name := strings.TrimSuffix(entry.Name(), ".expected.json") - if pattern.MatchString(name) { + if pattern.MatchString(name) || name == "connector_object_binding_flow" { if _, err := os.Stat(filepath.Join(conformanceDir, name+".sysml")); err != nil { continue } @@ -32,6 +32,7 @@ func TestRoutingUnchangedByConnectorObjects(t *testing.T) { } sort.Strings(cases) known := loadKnownFailures(t, conformanceDir) + forcedCases := make(map[string]bool) for _, name := range cases { name := name t.Run(name, func(t *testing.T) { @@ -48,10 +49,23 @@ func TestRoutingUnchangedByConnectorObjects(t *testing.T) { } runConformanceCase(t, conformanceDir, name, DefaultSchedulePolicy) if expected.Type != "instance" { - t.Log("no root instance exists before behavior execution; normal execution is the only applicable routing comparison") + if expected.Type != "state" || len(expected.Performers) == 0 { + runConformanceCaseWithOwned(t, conformanceDir, name, DefaultSchedulePolicy, true) + return + } + runConformanceCaseWithOwned(t, conformanceDir, name, DefaultSchedulePolicy, true) + forcedCases[name] = true return } runConformanceCaseWithOwned(t, conformanceDir, name, DefaultSchedulePolicy, true) + forcedCases[name] = true }) } + for _, name := range cases { + if strings.HasPrefix(name, "send_bind_relay_") || name == "connector_object_binding_flow" { + if !forcedCases[name] { + t.Fatalf("required routing differential case %q did not run under forceOwned", name) + } + } + } } diff --git a/internal/semantic/query/metamodel.go b/internal/semantic/query/metamodel.go index 1b1764c19..2ea6220eb 100644 --- a/internal/semantic/query/metamodel.go +++ b/internal/semantic/query/metamodel.go @@ -34,6 +34,7 @@ var metamodelTypeNames = map[symbols.SymbolKind]string{ symbols.SymbolEnumerationUsage: "EnumerationUsage", symbols.SymbolViewUsage: "ViewUsage", symbols.SymbolViewpointUsage: "ViewpointUsage", symbols.SymbolRenderingUsage: "RenderingUsage", symbols.SymbolConcernUsage: "ConcernUsage", symbols.SymbolConnectionUsage: "ConnectionUsage", + symbols.SymbolBindingUsage: "BindingConnectorAsUsage", symbols.SymbolSuccessionUsage: "SuccessionAsUsage", symbols.SymbolFlowUsage: "FlowUsage", symbols.SymbolPortUsage: "PortUsage", symbols.SymbolInterfaceUsage: "InterfaceUsage", symbols.SymbolAllocationUsage: "AllocationUsage", symbols.SymbolActionUsage: "ActionUsage", diff --git a/internal/semantic/semantics/annotations.go b/internal/semantic/semantics/annotations.go index 6d40eb602..1d0a0125e 100644 --- a/internal/semantic/semantics/annotations.go +++ b/internal/semantic/semantics/annotations.go @@ -1148,6 +1148,7 @@ var metaclassNames = map[symbols.SymbolKind]string{ symbols.SymbolConcernUsage: "ConcernUsage", symbols.SymbolConnectionDef: "ConnectionDefinition", symbols.SymbolConnectionUsage: "ConnectionUsage", + symbols.SymbolBindingUsage: "BindingConnectorAsUsage", symbols.SymbolSuccessionUsage: "SuccessionAsUsage", symbols.SymbolFlowDef: "FlowDefinition", symbols.SymbolFlowUsage: "FlowUsage", diff --git a/internal/semantic/semantics/filter.go b/internal/semantic/semantics/filter.go index ed1e19292..963daf476 100644 --- a/internal/semantic/semantics/filter.go +++ b/internal/semantic/semantics/filter.go @@ -533,7 +533,7 @@ func isFeaturingType(sym *symbols.Symbol) bool { symbols.SymbolMetadataUsage, symbols.SymbolViewUsage, symbols.SymbolViewpointUsage, symbols.SymbolRenderingUsage, symbols.SymbolConcernUsage, symbols.SymbolConnectionUsage, - symbols.SymbolSuccessionUsage, + symbols.SymbolBindingUsage, symbols.SymbolSuccessionUsage, symbols.SymbolFlowUsage, symbols.SymbolPortUsage, symbols.SymbolInterfaceUsage, symbols.SymbolAllocationUsage, symbols.SymbolActionUsage, symbols.SymbolStateUsage, diff --git a/internal/semantic/semantics/shape.go b/internal/semantic/semantics/shape.go index 5dba50da4..2427a0deb 100644 --- a/internal/semantic/semantics/shape.go +++ b/internal/semantic/semantics/shape.go @@ -208,16 +208,12 @@ func IsShapeFeature(sym *symbols.Symbol) bool { if sym == nil { return false } - if sym.Kind == symbols.SymbolUnknown { - usage, ok := sym.Decl.(*ast.Usage) - return ok && usage.Kind == ast.UsageBinding - } switch sym.Kind { case symbols.SymbolAttributeUsage, symbols.SymbolPartUsage, symbols.SymbolItemUsage, symbols.SymbolPortUsage, symbols.SymbolConnectionUsage, symbols.SymbolActionUsage, symbols.SymbolStateUsage, symbols.SymbolConstraintUsage, symbols.SymbolRequirementUsage, symbols.SymbolOccurrenceUsage, symbols.SymbolIndividualUsage, - symbols.SymbolInterfaceUsage, symbols.SymbolFlowUsage, + symbols.SymbolInterfaceUsage, symbols.SymbolFlowUsage, symbols.SymbolBindingUsage, // An allocation usage is a connection usage of the allocation library // (SysML v2 §8.3.19), so an object carries it as a feature. symbols.SymbolAllocationUsage: diff --git a/internal/semantic/symbols/builder.go b/internal/semantic/symbols/builder.go index 40949c713..6ccf54457 100644 --- a/internal/semantic/symbols/builder.go +++ b/internal/semantic/symbols/builder.go @@ -734,6 +734,8 @@ var usageSymbolKinds = map[ast.UsageKind]SymbolKind{ // (KerML 1.0 §7.4.6), so it is one kind of symbol. ast.UsageConnection: SymbolConnectionUsage, ast.UsageConnector: SymbolConnectionUsage, + // A binding is a BindingConnectorAsUsage (SysML v2 §8.3.13). + ast.UsageBinding: SymbolBindingUsage, // A succession is a SuccessionAsUsage (SysML v2 §8.3.13.7): a connector // usage of its own kind, so it is a redefinition target like any feature. ast.UsageSuccession: SymbolSuccessionUsage, diff --git a/internal/semantic/symbols/kind_mapping_test.go b/internal/semantic/symbols/kind_mapping_test.go index ef2fac6dd..be41f6d27 100644 --- a/internal/semantic/symbols/kind_mapping_test.go +++ b/internal/semantic/symbols/kind_mapping_test.go @@ -12,7 +12,7 @@ import ( // kindMappingDigest pins the declaration-kind → SymbolKind mapping, which the // on-disk library index persists and must be invalidated for when it changes. -const kindMappingDigest = "82ddcc33cf2bb4d3" +const kindMappingDigest = "6891b0021bc3879a" func TestSymbolKindMappingIsPinnedToTheIndexFormatVersion(t *testing.T) { var b strings.Builder diff --git a/internal/semantic/symbols/symbol.go b/internal/semantic/symbols/symbol.go index 0c8cfc8c1..6f69c2e32 100644 --- a/internal/semantic/symbols/symbol.go +++ b/internal/semantic/symbols/symbol.go @@ -64,6 +64,9 @@ const ( SymbolConcernUsage // Tier B usages. SymbolConnectionUsage + // SymbolBindingUsage classifies a binding usage: a BindingConnectorAsUsage + // (SysML v2 §8.3.13). + SymbolBindingUsage // SymbolSuccessionUsage classifies a succession usage: a SuccessionAsUsage // (SysML v2 §8.3.13.7), a ConnectorAsUsage that is not a ConnectionUsage. SymbolSuccessionUsage @@ -153,6 +156,7 @@ var symbolKindNames = map[SymbolKind]string{ SymbolRenderingUsage: "renderingUsage", SymbolConcernUsage: "concernUsage", SymbolConnectionUsage: "connectionUsage", + SymbolBindingUsage: "bindingUsage", SymbolSuccessionUsage: "successionUsage", SymbolFlowUsage: "flowUsage", SymbolPortUsage: "portUsage", @@ -196,7 +200,7 @@ func (k SymbolKind) IsFeature() bool { case SymbolPartUsage, SymbolAttributeUsage, SymbolItemUsage, SymbolOccurrenceUsage, SymbolIndividualUsage, SymbolMetadataUsage, SymbolEnumerationUsage, SymbolViewUsage, SymbolViewpointUsage, SymbolRenderingUsage, SymbolConcernUsage, SymbolConnectionUsage, - SymbolSuccessionUsage, SymbolFlowUsage, SymbolPortUsage, SymbolInterfaceUsage, + SymbolBindingUsage, SymbolSuccessionUsage, SymbolFlowUsage, SymbolPortUsage, SymbolInterfaceUsage, SymbolAllocationUsage, SymbolActionUsage, SymbolStateUsage, SymbolCalcUsage, SymbolConstraintUsage, SymbolRequirementUsage, SymbolSatisfyRequirementUsage, SymbolCaseUsage, SymbolAnalysisCaseUsage, SymbolVerificationCaseUsage, SymbolUseCaseUsage, diff --git a/internal/workspace/libs/record.go b/internal/workspace/libs/record.go index c2a5169f3..1925df08d 100644 --- a/internal/workspace/libs/record.go +++ b/internal/workspace/libs/record.go @@ -9,7 +9,7 @@ import ( // formatVersion is the on-disk record format version. Bump it whenever the // persisted shape changes; a change to what a record captures needs no bump, // since the build ID in the cache key already invalidates records (see buildid.go). -const formatVersion = 26 +const formatVersion = 27 // factRecord is the derived analysis persisted for one library symbol, named by // the fully-qualified name it is declared under. It holds no declaration and no diff --git a/internal/workspace/libs/snapshot.go b/internal/workspace/libs/snapshot.go index 8724209ef..ea7aa9bb4 100644 --- a/internal/workspace/libs/snapshot.go +++ b/internal/workspace/libs/snapshot.go @@ -28,7 +28,7 @@ var stdlibSnapshot []byte // whenever the stream layout does, so a stale blob is refused, not misread. const ( snapshotMagic = "OpenSysML library snapshot\n" - snapshotFormatVersion = 17 + snapshotFormatVersion = 18 ) // snapshotCRC checksums the index stream, so a damaged byte that still parses diff --git a/internal/workspace/libs/snapshot_test.go b/internal/workspace/libs/snapshot_test.go index 58ea15148..b28172cfe 100644 --- a/internal/workspace/libs/snapshot_test.go +++ b/internal/workspace/libs/snapshot_test.go @@ -79,7 +79,7 @@ func TestSnapshotIndexMatchesFreshLoad(t *testing.T) { // snapshotKindDigest pins the SymbolKind and PseudostateKind numbering the snapshot // stream persists under snapshotFormatVersion; a kind added or moved renumbers the ones after it. -const snapshotKindDigest = "115e3c0a56293855" +const snapshotKindDigest = "ed11b7ec256dac79" func TestSnapshotFormatVersionPinsSymbolKinds(t *testing.T) { var b strings.Builder diff --git a/internal/workspace/libs/stdlib.snapshot b/internal/workspace/libs/stdlib.snapshot index 2eed05580e772b3088828a4971809668e8487d6a..19a1ac31d0e792782d5d5798fe6c63fe00305842 100644 GIT binary patch delta 6664 zcmY*dd0bRg`#*PP#05+xgc)!_T=F9KfNN!zD+Pn7FKCvHlitiGq#Ko4@v}+M!9+q( z;wT~_ny8t(UX@ivAY1`OL|kye<)`Lbeyq&p{hm9L;~(eq{XFY=&U2pUId`Titx>py zmFg#Wrga+L;>kYsh%i@n$bv)GIb^*xnyE>yZ5%awmt}*&%mv$Xy+>zeDck zkOLfYcZb}=A@_91y>`hj*`tLU3mT(e!s@LyMvE(k35xn{y4Y0^V%f?c#mhphIyXb~ z)ceG)%QBEUuW9$Zj3b7TeS!T6A&Wg@*~aB!B5e9*ie_#zdxhxh8@mg-8-p5J76ch5 zL_>KL(Jjf)Wq8J_TUUrfx!&C@(HFkmSBkSZnUF1xhnLgY;+vf8k|QpLbaRgQfgmW% zGgs^@SlOst@w_mbbzdXCCCpZ5tP$h&uxVN+u7;BO{d%#E(^EHzGupxEXudeWq&!(^ zAgMcqN`a7iU9pZ71tM-jEX=n+^b;&>P=Qzi__IJ94ck$LI5EiH#o{+0bBo1(Er6#= z#rJp|eaplFfazu89g|X>Z6NjYbVuM>)hFwpdH$qa^fOvcLi~NAP9|jUbJCo0(c5UL zh4k10F3o>VdJNLZmO4n6EYwXSq|jA*2JHI_b<+tca%IrSR=P@?z|Q`Wvk$wnuYsME!P!S#+1J6opP^F-IqJ&30rnNf z*~eVjH^Hu6#@WX`T=^{LSDuPn5O2-o;;QGQ$2933NRzX;w8mAcKKxZYrR!o@xXM5- z?$$E(oBpa^Aj!LRWBx6&-;VGiQ{9h>v<1p)aJA_7on;qHQtSBbt~4yc9X+kc-C*CV z zV9Rw8D^T5Xz0d7(n6KO#^c|q{wjiTPIdQ;1D!w`iCndoJ!K7MC{r#?rI%Zo?iVj}R z?FjmW50i*pK{HzWDH#WGyQcNReBbtKL$X3hP2dfR=`q(|QLBoACcDFNaAnX}FvzP6 zN*jie^{Ts+feEIb@Q`})Np`=j6a#YV3)0Ucg*etWN}_-vQPOn4nkXq8a63xkVNq#prF zndAdVEJI|9YA8o!Ajej4uKH1hw4RggD>bFwm69E$cgjEtMn;v6^hzIbMzi>@UW%$5mV!`IY{gM29lk@3Ny*%I;c@8#VC4yEonTi- zRY`LNVLmIX#^ujv51c@2G)k_KtT_Ds8c7E9I4RA6;ZG+eUe}+Tk|e;2Q_@Jlg;UZ& zcw18|eI`J)W1W-$Smp%3Gty{KwlgR?D?5Hh>S$DIY7Hbye20*f(4cx3f}5$U7sH3IsG{QJoSq-zMnJE}N=R>PP$vrLK#FBd&?ek?K*cV*c z>bT3&?_J?!q@MDdOV!iw0PPL*BYgOLZJ;xCAaA?TElAL`%8?%-B|x2b

$#XH?WP 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 Date: Sun, 20 Sep 2026 19:11:34 +0000 Subject: [PATCH 3/3] test(runtime): simplify routing differential bookkeeping Co-Authored-By: jason.han --- internal/exec/runtime/routing_differential_test.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/internal/exec/runtime/routing_differential_test.go b/internal/exec/runtime/routing_differential_test.go index e199b7ef8..294036f2f 100644 --- a/internal/exec/runtime/routing_differential_test.go +++ b/internal/exec/runtime/routing_differential_test.go @@ -48,17 +48,12 @@ func TestRoutingUnchangedByConnectorObjects(t *testing.T) { t.Fatal(err) } runConformanceCase(t, conformanceDir, name, DefaultSchedulePolicy) - if expected.Type != "instance" { - if expected.Type != "state" || len(expected.Performers) == 0 { - runConformanceCaseWithOwned(t, conformanceDir, name, DefaultSchedulePolicy, true) - return - } - runConformanceCaseWithOwned(t, conformanceDir, name, DefaultSchedulePolicy, true) + // Only a case with an object before execution has connectors to force; + // the others skip inside the harness. + if expected.Type == "instance" || expected.Type == "state" && len(expected.Performers) > 0 { forcedCases[name] = true - return } runConformanceCaseWithOwned(t, conformanceDir, name, DefaultSchedulePolicy, true) - forcedCases[name] = true }) } for _, name := range cases {