feat(runtime): execute binding connectors - #310
Conversation
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>
Original prompt from Devin Bot
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
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>
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>
…utors Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
Summary
bindparsed and resolved but never executed:part def Sys { attribute a = 5; attribute b; binding bind b = a; }readb = <unknown>, andcalc def D2 { in x; bind result = x; }failed with no result expression. A binding asserts its two ends are the same thing (KerMLBindingConnector), 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: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
RelReferencesend (#301) — sou.Identmeant 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: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.materializeFeatureValueconsults bindings before itsMaterializedearly return (that is what makes a conflict between two already-valued ends an error rather than a silent winner), andContext.Instantiateis 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 existingGetFeatureValue.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 featurefof an object walksInstance.Owner()accumulating the qualified suffix (bof the object held aschildbecomeschild.b) and consults each ancestor's bindings for an end whose path is exactly that. Exactly, not by root segment —bind child.b = xis aboutbof the child object and aboutx, never aboutchilditself, 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:
ErrBindingConflict, naming both ends (as the model spells them) and both valuesbind a = b; bind b = a)ErrBindingCycle, no recursion into itmain:<unknown>/ErrUninitializedFeatureValue— one binding over a pair is not a cyclebind b = a + 1)ErrBindingEndErrBindingEnd, and only for a binding that actually involves the feature being read — an unrelated binding never fails a sibling readbind result = x,bind x = result)calcShapeOfbind p1 = p2)bind a = b; bind a = c)ErrBindingConflictover all bindings relevant to the end, rather than the first one winningErrBindingEndnaming the end, and a documented limitationThree 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
bhad takena's value, a later write ofa := 9made both ends look independently valued and every read failed with a conflict betweenaand a copy ofa. A conflict is between values the model states or a run writes, soFeatureValue.BindingDerivedmarks a slot filled by propagation the wayWrittenmarks run-assignment,SetFeatureValueclears 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 thevalueKeyFunchash 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 toattribute b = ("b")read back asa = ["b"].Setkeeps that hash as a bucket key with an exactvalueEqualconfirmation 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 sharedruntime.FormatValue, so a collection conflict readsbinding conflict: b = ["b"], a = ["a"]rather thanb = sequence, a = sequence; the REPL shares it, which also means%featuresprints a set's elements instead ofSet{3}. Propagation chargeschargeElementslike 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 (thebinding [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/%evalon the report's own repros, plus the cases above: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, andbinding_object_endasserts the two ends areidentical. Golden AST fixturebinding_anonymous_simplelocks the parse shape of both anonymous spellings, with a parser-level test for the end relationship and its span. Robustness cases inrobustness_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 -lclean;go test ./...and-racepass;make lintpasses;check-doc-links.py0 broken links; stdlib conformance 95/95; training corpus 98/100 clean (training_examples_expected.txtuntouched);TestExecutionTraceandTestExecutionConformancepass with no golden re-baselining.Coordination: no changes to
state_executor.go,action_executor.go,lower/state_graph.go, or the structure ofContext.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