Skip to content

feat(runtime): execute binding connectors - #310

Merged
HuiJun merged 14 commits into
mainfrom
devin/1787036202-binding-runtime
Aug 18, 2026
Merged

feat(runtime): execute binding connectors#310
HuiJun merged 14 commits into
mainfrom
devin/1787036202-binding-runtime

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 18, 2026

Copy link
Copy Markdown

Summary

bind parsed and resolved but never executed: part def Sys { attribute a = 5; attribute b; binding bind b = a; } read b = <unknown>, and calc def D2 { in x; bind result = x; } failed with no result expression. A binding asserts its two ends are the same thing (KerML BindingConnector), so this implements that: a value on either end is the value of both.

New lowered IR (internal/core/lower/binding.go) normalizes the binding spellings into two ends, each keeping both the runtime path and the lossless expression:

type Binding struct { Ends [2]BindingEnd; Scope *symbols.Scope; Decl *ast.Usage }
type BindingEnd struct { Path string; Expr ast.Node }   // Path "" ⇒ expression-only end

Normalizing them needed a fix one layer up. The parser filed a simple-name first end into the usage's own identification and recorded no relationship, while a qualified or chained end became a RelReferences end (#301) — so u.Ident meant either "the connector's name" (binding startBinding = startEvent.timestamp;) or "a stated end" (bind b = a;) depending on spelling, and no rule downstream can be right for both:

before          binding bind b = a;      ident="b",  0 relationships   → not lowered
                bind b = a;              ident="b",  0 relationships   → not lowered
                binding ba bind b = a;   ident="ba", references→b      → lowered
after           an end stated after `bind` is always a references end; ident stays empty

So a runtime binding exists exactly when the notation states both ends. A binding that states only one (binding someName = someValue;, bind x;, binding n of x;) asserts nothing and lowers to nothing — inferring an end from the connector's name would make a declaration's meaning depend on a name collision with a feature, and lowering a half-binding made an ordinary read of a perfectly good stated value fail with empty endpoint.

The runtime (internal/core/runtime/binding.go) resolves through that IR only — no syntax is re-derived in an executor. materializeFeatureValue consults bindings before its Materialized early return (that is what makes a conflict between two already-valued ends an error rather than a silent winner), and Context.Instantiate is structurally unchanged. Nothing is eager: an end is materialized only when a read reaches it, and intermediate objects of a chained end (bind a.b.c = d) are traversed through the existing GetFeatureValue.

An end may be nested (bind child.b = x), and then the binding is declared on the owner of the object holding the bound feature, so a read has to look outward: resolving feature f of an object walks Instance.Owner() accumulating the qualified suffix (b of the object held as child becomes child.b) and consults each ancestor's bindings for an end whose path is exactly that. Exactly, not by root segment — bind child.b = x is about b of the child object and about x, never about child itself, so reading the container does not enter binding resolution and cannot report a cycle against itself. Which end carries the feature being read is decided by object+slot identity rather than by name.

Rules applied to the open cases:

case rule
both ends valued, values differ ErrBindingConflict, naming both ends (as the model spells them) and both values
valueless ring over ≥2 bindings (bind a = b; bind b = a) ErrBindingCycle, no recursion into it
ring with a value anywhere resolves to that value
single binding, neither end valued unchanged from main: <unknown> / ErrUninitializedFeatureValue — one binding over a pair is not a cycle
end is an expression (bind b = a + 1) supplies a value; it cannot receive one, so propagating into it is ErrBindingEnd
end resolves to no feature ErrBindingEnd, and only for a binding that actually involves the feature being read — an unrelated binding never fails a sibling read
calc result (bind result = x, bind x = result) supplies the calc's result from the other end, read out of the lowered bindings in calcShapeOf
both ends hold objects (bind p1 = p2) the ends are one object: the end being read materializes, the other adopts it — a conflict only if both already hold distinct objects
a value that arrived through a binding, and the other end later changes re-derived, not compared — see below
two bindings over the same scalar end, disagreeing (bind a = b; bind a = c) ErrBindingConflict over all bindings relevant to the end, rather than the first one winning
several bindings contributing to one multi-valued end not implemented: typed ErrBindingEnd naming the end, and a documented limitation
binding states only one end asserts nothing; lowers to no binding

Three consequences of the identity rule are worth calling out, since each was silently wrong first.

Derived values. A propagated value is stored in the receiving slot, so once b had taken a's value, a later write of a := 9 made both ends look independently valued and every read failed with a conflict between a and a copy of a. A conflict is between values the model states or a run writes, so FeatureValue.BindingDerived marks a slot filled by propagation the way Written marks run-assignment, SetFeatureValue clears it, and a derived end is re-derived instead of compared.

Object ends. Object-valued ends are unified by not forcing the other end to materialize its own composite child while a read resolves — otherwise the resolution order manufactures two objects and reports them as a conflict the model never stated.

Comparison and rendering. Conflict detection compares values content-wise (valueEqual) instead of through the valueKeyFunc hash projection, which is under-discriminating by design (it hashes an element's kind and the low two bytes of an integer, ignoring string content) and so silently accepted disagreeing collections: attribute a = ("a") bound to attribute b = ("b") read back as a = ["b"]. Set keeps that hash as a bucket key with an exact valueEqual confirmation inside the bucket, so dedup is exact while insertion and membership stay expected O(1) (n distinct elements ⇒ n buckets, max bucket length 1). Values in a diagnostic now render through one shared runtime.FormatValue, so a collection conflict reads binding conflict: b = ["b"], a = ["a"] rather than b = sequence, a = sequence; the REPL shares it, which also means %features prints a set's elements instead of Set{3}. Propagation charges chargeElements like every other collection write, so a value flowing through a binding is bounded by the same budget.

Binding relevance is memoized per (type symbol, end path), so an unrelated read costs one map lookup; outcomes are deliberately not cached, because a later mutation of one end must be visible through the binding.

Known limitations, recorded in docs/project/spec-compliance.md: a binding owned directly by a package/namespace is not applied until namespace objects are materialized; a binding that states only one end asserts nothing; and element-wise contribution to a multi-valued end by several bindings (the binding [1] bind [0..1] tf.edges = [0..1] tfe; shape in the Geometry stdlib) is a typed error rather than a silent selection.

Verification

%features/%eval on the report's own repros, plus the cases above:

P::Fwd    a = 5, b = 5          %eval in P::Fwd : b  →  5
P::Short  a = 5, b = 5          (shorthand spelling: bind b = a)
P::Rev    a = 7, b = 7          (value flows the other way)
P::D2(11) = 11
P::Named  a = 5                 (binding bnd = a; states one end)
P::OneEnd x = 4                 (bind x; states no value)
P::Unset  a = <unknown>, b = <unknown>
P::Cyc    a: <error: binding cycle: b -> a>
P::Conf   b: <error: binding conflict: b = 2, a = 1>
P::Coll   b: <error: binding conflict: b = ["b"], a = ["a"]>
P::Objs   p1 = Instance(ID: 8), p2 = Instance(ID: 8)
P::Nest   x = 9, child.b = 9    (nested end: bind child.b = x)

Conformance cases added under runtime/testdata/conformance/: binding_value_forward, binding_value_reverse, binding_nested_end, binding_nested_end_reverse, binding_expression_end, binding_multivalued, binding_calc_result, binding_calc_result_reverse, binding_object_end. The harness reads a dotted slot key ("child.b"), so a nested end is asserted where it lives, and binding_object_end asserts the two ends are identical. Golden AST fixture binding_anonymous_simple locks the parse shape of both anonymous spellings, with a parser-level test for the end relationship and its span. Robustness cases in robustness_test.go: conflict messages (scalar and both collection hash-collision shapes, asserted on the rendered text), two already-materialized distinct objects, a derived value refreshed after the other end is written and the same through a binding chain, writes to both ends still conflicting, disagreeing and agreeing two-binding ends, multiple contributors to a multi-valued end, the element budget during propagation, 2-binding cycle, 3-binding ring, cycle with a value, valueless single binding, a one-end binding not poisoning a sibling read, expression end cannot receive, reading the container of a nested end is not a cycle.

Gate (local, on the merge of current main): go build, go vet, gofmt -l clean; go test ./... and -race pass; make lint passes; check-doc-links.py 0 broken links; stdlib conformance 95/95; training corpus 98/100 clean (training_examples_expected.txt untouched); TestExecutionTrace and TestExecutionConformance pass with no golden re-baselining.

Coordination: no changes to state_executor.go, action_executor.go, lower/state_graph.go, or the structure of Context.Instantiate (PR #289's files). #307 and #309 are merged in.

Link to Devin session: https://nasa-jpl-demo.devinenterprise.com/sessions/d86ebb1a46ae44d4abd9ab891cb3095b
Requested by: @HuiJun

devin-ai-integration Bot and others added 4 commits August 18, 2026 07:13
Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
@HuiJun HuiJun self-assigned this Aug 18, 2026
@devin-ai-integration

Copy link
Copy Markdown
Author
Original prompt from Devin Bot

# Task: three defects left behind by the binding-end parser fix

Repo: JPL-Devin/Systemica, which GitHub now serves as JPL-Devin/OpenSysML (the project was
renamed); checkout /home/ubuntu/repos/Systemica, Go module github.com/Open-MBEE/OpenSysML, env
vars OPENSYSML_*. Read AGENTS.md first and obey it — especially §1 (root cause first, never
weaken a test), §4 (immutable AST, semantics in side tables, runtime consumes lowered IR, error
timing is contract), §5.2 (the four-layer behavioral test contract) and §8 (implement completely, and
every unsupported path returns a typed error rather than a silent no-op).

Branch from current origin/main (ed2574cf or later). PR #301 just landed there: a bind end is
now read as a connector end, so a qualified name or a chain of qualified names is accepted, recorded
as nested FeatureChainExpr, and resolved segment by segment; the first end is a reference
subsetting
(ast.RelReferences), not a redefinition. All three items below were found while
verifying that work, and I reproduced each on a binary built from the commit before it — so none is
a regression, and none is fixed by it.

Do these in the order given: item 1 is a hang, and item 2's tests will exercise chains.


#``# Item 1 — feature-chain resolution does not terminate on a long chain

A 30-segment dotted chain does not finish in 25 seconds (timeout 25 → exit 124); ~20 segments takes
~0.9s, so cost is super-linear, not merely slow. Reproducer, which I verified hangs on both
ed2574cf and its parent:

segs = 30
body = "".join(f"part p{i} {{ " for i in range(segs)) + "part leaf;" + "}" * segs
chain = ".".join(f"p{i}" for i in range(segs)) + ".leaf"
open("/tmp/chain30.sysml", "w").write(f"package C {{\n  part A {{ {body} }}\n  part b;\n  binding bind A.{chain} = b;\n}}\n")

Start at Resolver.resolveFeatureChain (internal/core/resolve/document.go:904). A chain is nested
left-recursively, and that function resolves `fc.Ope... (8456 chars truncated...)

@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

devin-ai-integration Bot and others added 2 commits August 18, 2026 13:27
Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 2 commits August 18, 2026 14:20
Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 2 commits August 18, 2026 14:52
…utors

Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
@HuiJun
HuiJun merged commit 628fe03 into main Aug 18, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant