Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@ is described in [docs/project/releasing.md](docs/project/releasing.md).

## Unreleased

### Binding connector runtime semantics

- **Bindings declared in materialized type and usage bodies now propagate values in both
directions**, including inherited and nested ends, with typed conflict and cycle errors;
calc result bindings such as `bind result = x` are also evaluated. Package-owned bindings
remain a documented limitation.

### A named control node is a member, and a chained binding declares none

- **`fork`, `join`, `merge` and `decision` register the name they declare**, the way `first`/`done`
already do, so `first Jump then Land;` names a control node as source or target instead of
reporting it unresolved. An unnamed control node declares no name and registers nothing.
- **A binding's end no longer names the binding.** `bind a.b.c = d;` records `a.b.c` as a reference
subsetting — the end it binds, not a name the binding answers to — so `%search` and the symbol
table no longer carry a stray `c` in the binding's owner.
- **A binding's end no longer names the binding.** `bind a.b.c = d;` records `a.b.c` as a reference
subsetting — the end it binds, not a name the binding answers to — so `%search` and the symbol
table no longer carry a stray `c` in the binding's owner.

### A view renders

Expand Down
2 changes: 1 addition & 1 deletion docs/project/spec-compliance.md
Original file line number Diff line number Diff line change
Expand Up @@ -901,7 +901,7 @@ semantics layer over the conjugation parity of the typing/specialization chain.
| 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 `<unknown>`, 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 `runtime/shape.go` `isFeature` (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 source/target relationships — rather than in a `connect` clause | `parser/defusage.go` `parseFlowEnds` and the `UsageBinding` clause; `lower/connection.go` (flow ends reach routing through lowering); `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` | ❌ Not implemented as a runtime connector object (a flow between action nodes carries its value, see the named-flow row above; what is missing is the connector *object*: a flow is a transfer performance carrying a payload and a binding makes one value of two features, so neither is materialized by `runtime/connector.go` and a feature value holding one reads as unknown. Materializing them means giving a flow its payload transfer semantics and a binding its value identity, which is separate work) |
| 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 source/target relationships — rather than in a `connect` clause | `parser/defusage.go` `parseFlowEnds` and the `UsageBinding` clause; `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`/`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`, `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, expression-valued endpoints, exact sequence/set equality, multiplicity handling, element-budget charging during propagation, lazy adoption of an unmaterialized composite endpoint by the read-side object, conflict/cycle errors, and calc result binding. A named binding that states only one end — including `binding bnd = x;`, `bind x;`, and `binding bnd of x;` — lowers to no runtime binding, 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.) |
| 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` (the `UsageBinding` `of` clause records `ast.RelReferences`) | `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) |
Expand Down
73 changes: 73 additions & 0 deletions internal/core/lower/binding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package lower

import (
"github.com/Open-MBEE/OpenSysML/internal/core/ast"
"github.com/Open-MBEE/OpenSysML/internal/core/symbols"
)

// Binding is a lowered binding connector with its two endpoint expressions and
// the scope in which those expressions were declared.
type Binding struct {
Ends [2]BindingEnd
Scope *symbols.Scope
Decl *ast.Usage
}

// BindingEnd is one binding endpoint. Path is the runtime lvalue path; Expr
// retains the lossless expression for diagnostics and calc evaluation.
type BindingEnd struct {
Path string
Expr ast.Node
}

// ToBindings lowers binding connectors directly declared by a type or usage.
// Namespace-owned bindings are intentionally left to callers to exclude.
func ToBindings(decl ast.Node, scope *symbols.Scope) []Binding {
var members []ast.Node
switch n := decl.(type) {
case *ast.Usage:
members = n.Members
case *ast.Definition:
members = n.Members
default:
return nil
}

var out []Binding
for _, member := range members {
u, ok := unwrapMembership(member).(*ast.Usage)
if !ok || u.Kind != ast.UsageBinding {
continue
}
binding, ok := lowerBinding(u, scope)
if ok {
out = append(out, binding)
}
}
return out
}

func lowerBinding(u *ast.Usage, scope *symbols.Scope) (Binding, bool) {
if u == nil {
return Binding{}, false
}

var first ast.Node
for _, rel := range u.Relationships {
if rel != nil && rel.Kind == ast.RelReferences {
first = rel.Target
break
}
}
if first == nil {
return Binding{}, false
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
if u.Value == nil {
return Binding{}, false
}
ends := [2]BindingEnd{
{Path: FeaturePath(first), Expr: first},
{Path: FeaturePath(u.Value), Expr: u.Value},
}
return Binding{Ends: ends, Scope: scope, Decl: u}, true
}
116 changes: 116 additions & 0 deletions internal/core/lower/binding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package lower

import (
"testing"

"github.com/Open-MBEE/OpenSysML/internal/core/ast"
"github.com/Open-MBEE/OpenSysML/internal/core/parser"
"github.com/Open-MBEE/OpenSysML/internal/core/source"
"github.com/Open-MBEE/OpenSysML/internal/core/symbols"
)

func TestToBindingsNormalizesBindingSpellings(t *testing.T) {
p := parser.New(source.New("binding.sysml", []byte(`package P {
part def Owner {
binding bind x = y;
bind x = y;
binding named bind x = y;
binding namedOf of x = y;
binding namedOnly = x;
bind incomplete;
binding incompleteOf of x;
binding [1] config.host = serverAddress;
}
}`)))
file := p.ParseFile()
idx := symbols.NewIndex()
idx.AddDocument("binding.sysml", file)
scope := idx.DocumentRoot("binding.sysml")
var bindings []Binding
for _, member := range file.Members {
membership, ok := member.(*ast.Membership)
if !ok {
continue
}
pkg, ok := membership.Member.(*ast.Package)
if !ok {
continue
}
for _, nested := range pkg.Members {
ownerMembership, ok := nested.(*ast.Membership)
if !ok {
continue
}
owner, ok := ownerMembership.Member.(*ast.Definition)
if ok {
bindings = append(bindings, ToBindings(owner, scope)...)
}
}
}
if len(bindings) != 5 {
t.Fatalf("lowered %d bindings, want 5", len(bindings))
}
wants := map[[2]string]int{
{"x", "y"}: 4,
{"config.host", "serverAddress"}: 1,
}
for _, binding := range bindings {
paths := [2]string{binding.Ends[0].Path, binding.Ends[1].Path}
if wants[paths] == 0 {
t.Errorf("binding paths = %q, want one of [x, y] or [config.host, serverAddress]", paths)
continue
}
wants[paths]--
}
for paths, count := range wants {
if count != 0 {
t.Errorf("binding paths %q occurred %d extra times", paths, count)
}
}
}

func TestToBindingsKeepsMultipleContributors(t *testing.T) {
p := parser.New(source.New("binding-multiple.sysml", []byte(`package P {
part def Sys {
part edges : Edge[*];
part leftEdge : Edge;
part rightEdge : Edge;
binding [1] bind [0..1] edges = [0..1] leftEdge;
binding [1] bind [0..1] edges = [0..1] rightEdge;
}
}`)))
file := p.ParseFile()
idx := symbols.NewIndex()
idx.AddDocument("binding-multiple.sysml", file)
scope := idx.DocumentRoot("binding-multiple.sysml")
var bindings []Binding
for _, member := range file.Members {
membership, ok := member.(*ast.Membership)
if !ok {
continue
}
pkg, ok := membership.Member.(*ast.Package)
if !ok {
continue
}
for _, nested := range pkg.Members {
ownerMembership, ok := nested.(*ast.Membership)
if !ok {
continue
}
owner, ok := ownerMembership.Member.(*ast.Definition)
if ok {
bindings = append(bindings, ToBindings(owner, scope)...)
}
}
}
if len(bindings) != 2 {
t.Fatalf("lowered %d bindings, want 2", len(bindings))
}
for _, binding := range bindings {
if got := [2]string{binding.Ends[0].Path, binding.Ends[1].Path}; got != [2]string{"edges", "leftEdge"} &&
got != [2]string{"edges", "rightEdge"} {
t.Errorf("binding paths = %q, want edges/leftEdge or edges/rightEdge", got)
}
}
}
51 changes: 51 additions & 0 deletions internal/core/parser/binding_of_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,57 @@ func TestBindingOfTargetIsAReference(t *testing.T) {
}
}

func TestAnonymousBindingSimpleEndsAreReferences(t *testing.T) {
const code = `package P {
binding bind b = a;
bind b = a;
}`

sf := source.New("test.sysml", []byte(code))
file := New(sf).ParseFile()
var bindings []*ast.Usage
var collect func([]ast.Node)
collect = func(nodes []ast.Node) {
for _, node := range nodes {
switch v := node.(type) {
case *ast.Membership:
collect([]ast.Node{v.Member})
case *ast.Package:
collect(v.Members)
case *ast.Usage:
if v.Kind == ast.UsageBinding {
bindings = append(bindings, v)
}
collect(v.Members)
}
}
}
collect(file.Members)
if len(bindings) != 2 {
t.Fatalf("parsed %d bindings, want 2", len(bindings))
}
for _, binding := range bindings {
if binding.Ident.Name != "" {
t.Errorf("binding ident = %q, want empty", binding.Ident.Name)
}
if len(binding.Relationships) != 1 {
t.Fatalf("binding has %d relationships, want 1", len(binding.Relationships))
}
rel := binding.Relationships[0]
if rel.Kind != ast.RelReferences {
t.Errorf("binding relationship kind = %v, want references", rel.Kind)
}
target, ok := rel.Target.(*ast.QualifiedName)
if !ok || len(target.Parts) != 1 || target.Parts[0].Text != "b" {
t.Errorf("binding target = %#v, want qualified name b", rel.Target)
continue
}
if sf.Text(target.Parts[0].Span) != "b" {
t.Errorf("binding target span = %#v, want source span for b", target)
}
}
}

func findBindingUsage(t *testing.T, root *ast.RootNamespace) *ast.Usage {
t.Helper()

Expand Down
18 changes: 18 additions & 0 deletions internal/core/parser/defusage.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ var notKindPrefixKeywords = map[string]bool{
"actor": true, "expose": true, "render": true, "perform": true,
"include": true, "exhibit": true, "variant": true, "event": true,
"timeslice": true, "snapshot": true, "transition": true, "bind": true,
"binding": true,
// `individual part p` keeps the modifier; the prefix path would drop it.
"individual": true,
"in": true, "out": true, "inout": true,
Expand Down Expand Up @@ -721,6 +722,10 @@ func (p *Parser) parseDefUsage(start int) ast.Node {
mods.isVariant = true
}
isAll := p.acceptKeyword("all")
if kw == "bind" {
u := p.parseUsage(start, usageKindKeywords[kw], kw, mods, isAll)
return applyPrefixes(normalizeAnonymousBindingEnd(u))
}

// `render` names the rendering a view uses (ViewRenderingMember) and
// `frame` the concern a requirement frames (FramedConcernMember). Each
Expand Down Expand Up @@ -2785,6 +2790,19 @@ func bindingEnd(target ast.Node) *ast.Relationship {
return &ast.Relationship{Kind: ast.RelReferences, Target: target}
}

func normalizeAnonymousBindingEnd(u *ast.Usage) *ast.Usage {
if u == nil || u.Kind != ast.UsageBinding || u.Ident.Name == "" || len(u.Relationships) != 0 {
return u
}
target := &ast.QualifiedName{
Parts: []ast.NameSegment{{Text: u.Ident.Name, Span: u.Ident.NameSpan}},
}
target.NodeSpan = u.Ident.NameSpan
u.Relationships = append(u.Relationships, bindingEnd(target))
u.Ident = ast.Identification{}
return u
}

// parseRelationshipTarget parses a relationship target which can be either:
// - A qualified name (A::B::C)
// - A feature chain (A.B.C or A::B.C.D - mix of :: and .)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
(RootNamespace
(Membership visibility="default"
(Package name="P" library=false standard=false
(Membership visibility="default"
(Definition kind="part" abstract=false variation=false name="Sys"
(Membership visibility="default"
(Usage kind="binding" name="" ref=false direction="none" composite=false derived=false ordered=false nonunique=false
(Relationship kind="references" target=b
(*ast.QualifiedName))
(FeatureReference name="a")))
(Membership visibility="default"
(Usage kind="binding" name="" ref=false direction="none" composite=false derived=false ordered=false nonunique=false keyword="bind"
(Relationship kind="references" target=b
(*ast.QualifiedName))
(FeatureReference name="a"))))))))
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package P {
part def Sys {
binding bind b = a;
bind b = a;
}
}
Loading
Loading