behavior-contracts 0.8.3 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/authoring.d.ts +1 -1
  2. package/dist/authoring.d.ts.map +1 -1
  3. package/dist/authoring.js +41 -2
  4. package/dist/authoring.js.map +1 -1
  5. package/dist/behavior.d.ts +0 -24
  6. package/dist/behavior.d.ts.map +1 -1
  7. package/dist/behavior.js +24 -2
  8. package/dist/behavior.js.map +1 -1
  9. package/dist/generator/emit-shared-go-typed.d.ts.map +1 -1
  10. package/dist/generator/emit-shared-go-typed.js +7 -7
  11. package/dist/generator/emit-shared-go-typed.js.map +1 -1
  12. package/dist/generator/emit-shared-rust-typed.d.ts +8 -0
  13. package/dist/generator/emit-shared-rust-typed.d.ts.map +1 -1
  14. package/dist/generator/emit-shared-rust-typed.js +20 -6
  15. package/dist/generator/emit-shared-rust-typed.js.map +1 -1
  16. package/dist/generator/emit-shared-typescript.d.ts.map +1 -1
  17. package/dist/generator/emit-shared-typescript.js +18 -2
  18. package/dist/generator/emit-shared-typescript.js.map +1 -1
  19. package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
  20. package/dist/generator/emit-typed-native-go.js +231 -606
  21. package/dist/generator/emit-typed-native-go.js.map +1 -1
  22. package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
  23. package/dist/generator/emit-typed-native-rust.js +287 -648
  24. package/dist/generator/emit-typed-native-rust.js.map +1 -1
  25. package/dist/generator/native-expr.d.ts +91 -7
  26. package/dist/generator/native-expr.d.ts.map +1 -1
  27. package/dist/generator/native-expr.js +124 -19
  28. package/dist/generator/native-expr.js.map +1 -1
  29. package/dist/generator/typed.d.ts +26 -0
  30. package/dist/generator/typed.d.ts.map +1 -1
  31. package/dist/generator/typed.js +52 -0
  32. package/dist/generator/typed.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/package.json +2 -2
@@ -2,7 +2,7 @@ import { GeneratorFailure } from "./core.js";
2
2
  import { isControlGate } from "../behavior.js";
3
3
  import { goStraightlineInternals } from "./emit-shared-go.js";
4
4
  import { goTypedInternals } from "./emit-shared-go-typed.js";
5
- import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
5
+ import { buildTypePlan, deriveTypeRef, findDecl, inputPortIsOptional, inputPortTypeRef } from "./typed.js";
6
6
  import { concurrencyPlan, execPlan, analyzeSequentialOps, classifyExpr, buildComponentPlan, } from "./straightline.js";
7
7
  import { NativeExprCompiler } from "./native-expr.js";
8
8
  const PKG = "dslcontracts";
@@ -49,161 +49,70 @@ function portsStructName(compName, nodeId) {
49
49
  // type to implement the per-component Handler_<comp> interface's concrete Node_* method signatures.
50
50
  return `PortsNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
51
51
  }
52
- function inferPortType(node, inputPorts, asBinding) {
53
- if (staticArrElems(node) !== null)
54
- return "Value";
55
- const e = classifyExpr(node);
56
- switch (e.kind) {
57
- case "str":
58
- case "concat":
59
- return "string";
60
- case "bool":
61
- return "bool";
62
- case "ref": {
63
- // #108: a `$as`-headed ref (map element binding) — resolve the element field's scalar type.
64
- if (asBinding !== undefined && e.path[0] === asBinding.name) {
65
- try {
66
- const { ref } = goTypedInternals.typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan);
67
- if (ref.kind === "scalar" && ref.scalar !== "null")
68
- return goScalarPortField(ref.scalar);
69
- }
70
- catch {
71
- return "Value";
72
- }
73
- return "Value";
74
- }
75
- if (e.path.length === 1) {
76
- const s = inputPorts[e.path[0]];
77
- if (s) {
78
- if (s.type === "string" || s.type === "literal")
79
- return "string"; // literal = constrained string.
80
- if (s.type === "int")
81
- return "int64";
82
- if (s.type === "float" || s.type === "number")
83
- return "float64"; // `"number"` = the float channel.
84
- if (s.type === "bool")
85
- return "bool";
86
- }
87
- }
88
- return "Value";
89
- }
90
- default:
91
- return "Value";
92
- }
93
- }
94
- /** scalar TypeRef → PortFieldType (native go scalar kind). */
95
- function goScalarPortField(scalar) {
96
- return scalar === "string" ? "string" : scalar === "int" ? "int64" : scalar === "float" ? "float64" : "bool";
97
- }
98
- /** A static string-array port (every element a static string literal) — the graphddb `projection`
99
- * shape. Returns the list of literal strings, or null when the node is not that shape. */
100
- function staticStringArrElems(node) {
101
- const arr = staticArrElems(node);
102
- if (arr === null)
103
- return null;
104
- const out = [];
105
- for (const el of arr) {
106
- const e = classifyExpr(el);
107
- if (e.kind !== "str")
108
- return null;
109
- out.push(e.value);
110
- }
111
- return out;
112
- }
113
52
  /**
114
- * portFieldGoType — the NATIVE Go type for a covered port field (bc#90 / runtime-free). The covered
115
- * ports struct carries NO `dslcontracts.Value` field: every port lowers to a concrete Go type.
116
- * - a static string-array (projection) `[]string` (change #2);
117
- * - a bare int/float number literal (e.g. Query `limit: 20`) `int64`/`float64`;
118
- * - a string/bool/scalar-input port the scalar Go type (inferPortType).
119
- * A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
120
- * Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no
121
- * `dslcontracts.Value` fallback on the native module). `where` names the emission site for the error.
53
+ * portFieldGoType — the NATIVE Go type for a covered port field (bc#90 / runtime-free), read off the ONE
54
+ * port lowering (compilePortNode / native-expr). The covered ports struct carries NO `dslcontracts.Value`
55
+ * field: every port lowers to a concrete Go type, and a port that cannot lower FAILS CLOSED. The field
56
+ * type is the compiled expression's TypeRef, so it can never disagree with the initializer (emitPortInit),
57
+ * which is the SAME lowering read for its value. `where` names the emission site for the error.
122
58
  */
123
- function portFieldGoType(node, inputPorts, where = "port", asBinding, plan, typedNodes) {
124
- if (staticStringArrElems(node) !== null)
125
- return "[]string";
126
- // #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → []ElemT.
127
- const arrElem = portArrayElemRef(node, inputPorts, plan);
128
- if (arrElem !== null)
129
- return `[]${renderTypeRef(arrElem)}`;
130
- // #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) → []ElemT.
131
- const priorArr = priorNodeArrElemInfo(node, typedNodes, plan);
132
- if (priorArr !== null)
133
- return `[]${renderTypeRef(priorArr.elemRef)}`;
134
- const num = numLiteralGoExpr(node);
135
- if (num !== null)
136
- return Number.isInteger(node) ? "int64" : "float64";
137
- // a ref to a `$as` element field (or an input map port) whose type is a MAP or NAMED struct (NOT a bare
138
- // scalar) → the native map/struct type (#108-map: a `{map:V}` write port lowers to map[string]V).
139
- const composite = portCompositeRef(node, inputPorts, asBinding, plan);
140
- if (composite !== null)
141
- return renderTypeRef(composite);
142
- const ft = inferPortType(node, inputPorts, asBinding);
143
- if (ft === "Value") {
144
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): ${where} '${JSON.stringify(node)}' does not lower to a native Go type (would need a boxed dslcontracts.Value). The covered read plane is runtime-free and admits NO boxed escape — the covered corpus ports are string / bool / static-string-array (projection) / bare-number (limit) only. Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
145
- }
146
- return ft;
59
+ function portFieldGoType(node, comp, plan, typedNodes, where = "port", opts) {
60
+ const c = compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), where, opts);
61
+ return c.renderedType ?? renderTypeRef(c.ref);
147
62
  }
148
- /** portCompositeRef (go twin) — the full TypeRef of a port that is a `{ref:[...]}` into a `$as` element
149
- * MAP or NAMED field (or an input MAP port with elemType). Returns null otherwise (caller falls through
150
- * to scalar/array inference). #108-map: enables a `{map:V}` write port to lower to map[string]V. */
151
- function portCompositeRef(node, inputPorts, asBinding, plan) {
152
- const e = classifyExpr(node);
153
- if (e.kind !== "ref" || plan === undefined)
154
- return null;
155
- let ref = null;
156
- if (asBinding !== undefined && e.path[0] === asBinding.name) {
157
- try {
158
- ref = goTypedInternals.typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan).ref;
159
- }
160
- catch {
161
- return null;
63
+ /**
64
+ * goNativeResolveHead the ONE ref-head resolver the runtime-free native-expr compiler reads, for cond
65
+ * `if`/branches, a map/fanout `when` guard, AND a port (go twin Go's value semantics need no ownership
66
+ * ceremony). A head is a `$as` element binding, a prior body node's typed local, or an input port.
67
+ */
68
+ function goNativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
69
+ return (head) => {
70
+ if (opts?.asBinding !== undefined && opts.asBase !== undefined && head === opts.asBinding.name) {
71
+ return { expr: opts.asBase, ref: opts.asBinding.ref };
162
72
  }
73
+ if (isPrior(head))
74
+ return { expr: typedLocal(head), ref: typedNodes.get(head) };
75
+ return goInputHeadRef(head, comp, plan);
76
+ };
77
+ }
78
+ /**
79
+ * compilePortNode — lower a port expression to its native ports-struct field via the runtime-free
80
+ * native-expr compiler (native-expr.ts): the ONE port lowering. The port's field TYPE is the compiled
81
+ * `ref`, its initializer the compiled `expr` — so the two are derived from a SINGLE lowering and can
82
+ * never disagree. Any closed operator, a literal, a ref (input scalar/array/map, prior-node scalar/array,
83
+ * `$as` element field), a concat, a static array, an optional-input read — all resolve here. A port that
84
+ * cannot lower FAILS CLOSED loudly. A lowering that is FALLIBLE (hoists statements) is a covered SHAPE
85
+ * returned here as-is; only the value site (emitPortInit) rejects it, since a ports-struct literal cannot
86
+ * host statements — the coverage gate/type site must still see it as covered.
87
+ */
88
+ function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
89
+ const resolveHead = goNativeResolveHead(comp, plan, typedNodes, isPrior, opts);
90
+ try {
91
+ // zeroOut is unreachable: a fallible port is rejected at emitPortInit (a ports-struct literal hosts no
92
+ // statements), so the backend never renders an early-return with it.
93
+ return new NativeExprCompiler(makeGoExprBackend("", resolveHead, plan)).compilePort(node);
163
94
  }
164
- else if (e.path.length === 1) {
165
- const s = inputPorts[e.path[0]];
166
- if (s !== undefined && s.type === "map") {
167
- const et = s.elemType;
168
- if (et !== undefined)
169
- return { kind: "map", value: deriveTypeRef(et, plan) };
170
- }
171
- return null;
95
+ catch (e) {
96
+ if (!(e instanceof GeneratorFailure))
97
+ throw e;
98
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} ${JSON.stringify(node)} does not lower to a native Go value: ${e.message}`);
172
99
  }
173
- if (ref === null)
174
- return null;
175
- if (ref.kind === "map" || ref.kind === "named")
176
- return ref;
177
- return null;
178
100
  }
179
- /** emitCompositePortValue (go twin) — the OWNED native value expr for a `{map:V}`/named port ref: a typed
180
- * field access `<elemBase>.<field>` (native map/struct, ZERO boxed Value). `elemBaseExpr` is the map arm's
181
- * element base for a `$as`-headed ref (omitted at top level — only input map-port refs resolve there). */
182
- function emitCompositePortValue(node, inputPorts, asBinding, plan, elemBaseExpr) {
183
- const e = classifyExpr(node);
184
- if (e.kind !== "ref")
185
- return null;
186
- if (asBinding !== undefined && elemBaseExpr !== undefined && e.path[0] === asBinding.name) {
187
- let acc;
101
+ /** the prior-node typed-local TypeRef map for a component (control-gate nodes are typeless; a map node
102
+ * carries its produced-array ref). Shared by coverage, the struct surface, and the runner. */
103
+ function buildTypedNodes(comp, plan) {
104
+ const typedNodes = new Map();
105
+ for (const n of comp.body) {
106
+ if (isControlGate(n))
107
+ continue;
188
108
  try {
189
- acc = goTypedInternals.typedFieldAccess(elemBaseExpr, asBinding.ref, e.path.slice(1), plan);
109
+ typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
190
110
  }
191
111
  catch {
192
- return null;
193
- }
194
- if (acc.ref.kind !== "map" && acc.ref.kind !== "named")
195
- return null;
196
- return acc.expr;
197
- }
198
- // a WHOLE-port ref to an input MAP port (with elemType) — valid at top level OR inside a map node (a
199
- // ref head that is NOT the `$as` element binding). Reads the native map[string]V off the input struct.
200
- if ((asBinding === undefined || e.path[0] !== asBinding.name) && e.path.length === 1) {
201
- const s = inputPorts[e.path[0]];
202
- if (s !== undefined && s.type === "map" && s.elemType !== undefined) {
203
- return `in.${goFieldName(e.path[0])}`;
112
+ /* untyped node — omit */
204
113
  }
205
114
  }
206
- return null;
115
+ return typedNodes;
207
116
  }
208
117
  function staticArrElems(node) {
209
118
  if (node === null || typeof node !== "object" || Array.isArray(node))
@@ -214,90 +123,16 @@ function staticArrElems(node) {
214
123
  const arg = node.arr;
215
124
  return Array.isArray(arg) ? arg : null;
216
125
  }
217
- /**
218
- * portArrayElemRef (#110) resolve a componentRef input port that binds a RUNTIME ARRAY (an IN-list /
219
- * array-bound WHERE head) to its native element TypeRef, or null when the port is not a covered array.
220
- * The covered shape is a SINGLE-SEGMENT, non-opt `ref` into an INPUT ARRAY port that carries a resolvable
221
- * `elemType` (scp-ir-architecture.md §5.2 / PortSchema.elemType — BC does NOT infer, consumer-interface
222
- * C3). The port then lowers to a native `[]ElemT` field fed straight from `in.<Field>` ZERO boxed Value
223
- * on the read hot path. This REUSES the bc#108 `elemType` element lowering (deriveTypeRef over the input
224
- * port's elemType), extended from the `map…over` element-source path to a plain componentRef port.
225
- *
226
- * A static string array is a DIFFERENT covered shape ([]string, staticStringArrElems) — handled ahead of
227
- * this. An array port with NO resolvable elemType, an opt/multi-segment ref, or a non-array input schema
228
- * resolves to null here → the caller keeps its existing fail-closed behaviour (never a silent box).
229
- * Requires a TypePlan (an obj elemType maps to a named decl); without one it stays null (fail-closed).
230
- */
231
- function portArrayElemRef(node, inputPorts, plan) {
232
- if (plan === undefined)
233
- return null;
234
- if (staticStringArrElems(node) !== null)
235
- return null; // the static-string-array port ([]string) — not this shape.
236
- const e = classifyExpr(node);
237
- if (e.kind !== "ref" || e.opt || e.path.length !== 1)
238
- return null;
239
- const schema = inputPorts?.[e.path[0]];
240
- if (schema === undefined || (schema.type !== "array" && schema.type !== "arr"))
241
- return null;
242
- const et = schema.elemType;
243
- if (et === undefined)
244
- return null;
245
- return deriveTypeRef(et, plan);
246
- }
247
- /**
248
- * isNativeRawComponent — a component covered by the combined read emitter. Strictly sequential
249
- * (no real concurrency), body is ALL componentRef (no map/cond), default 'fail'/'continue'
250
- * policy, relationKind is single/connection or absent, and EVERY node carries outType + the
251
- * component carries outputType. relationKind:single / bindField ARE the point — inline exec makes
252
- * the child-present decision from the real parent result. Ports AND the output are evaluated via
253
- * the primitive SSoT over a Value-space portScope (so a dynamic operator in a port or an
254
- * obj-assembly output is fine — the SSoT handles fail-closed semantics; only the RESULT plane +
255
- * PORT container go native). A covered SHAPE that lacks outType is a fail-closed error raised at
256
- * emit (see assertTypedCoverage) — never a silent re-box.
257
- */
258
- /**
259
- * numLiteralGoExpr — a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
260
- * `classifyExpr` deliberately leaves a bare number `dynamic` (its checked int/float range rules are
261
- * the evaluate SSoT, expr.go evalNumberLiteral), so a plain number never reaches the str/bool/null
262
- * static branch. But a number literal IS statically resolvable to a native Go Value: the SAME
263
- * range check the runtime applies is performed HERE at generation time (integral literal in the
264
- * safe 2^53-1 window → int64; a fractional/exponent literal → the checked float64). Returns the Go
265
- * expression for the literal's Value, or null when the node is not a bare number OR the integral
266
- * literal is out of the safe window (that out-of-range case keeps the runtime INVALID_LITERAL
267
- * semantics — the port is NOT covered natively, so the whole component falls through, never a
268
- * silently-wrong inline). Mirrors coderuntime.evalNumberLiteral exactly.
269
- */
270
- const NUM_SAFE_INT = 9007199254740991; // 2^53 - 1 (expr.go safeInt)
271
- function numLiteralGoExpr(node) {
272
- if (typeof node !== "number" || !Number.isFinite(node))
273
- return null;
274
- if (Number.isInteger(node)) {
275
- if (node < -NUM_SAFE_INT || node > NUM_SAFE_INT)
276
- return null; // out of safe window → not covered (runtime INVALID_LITERAL)
277
- return `${PKG}.Value(int64(${node}))`;
126
+ /** Does a port lower to a native value? The ELIGIBILITY gate ASKS the one port lowering (compilePortNode
127
+ * / native-expr) whether it can lower this port, rather than re-deriving the shape rules so the gate
128
+ * and the emitter can never disagree. A port that does not lower (or lowers fallibly) fails closed. */
129
+ function portIsStatic(node, comp, plan, typedNodes, asBinding) {
130
+ try {
131
+ compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), "port", asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined);
132
+ return true;
278
133
  }
279
- // fractional / exponent literal → checked float64 (finite, already guaranteed above).
280
- return `${PKG}.Value(float64(${node}))`;
281
- }
282
- /** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
283
- * resolvable to a native Go Value with no NewObj / no evaluate interpreter. */
284
- function portIsStatic(node) {
285
- const arr = staticArrElems(node);
286
- if (arr !== null)
287
- return arr.every(portIsStatic);
288
- if (numLiteralGoExpr(node) !== null)
289
- return true; // bare number literal (e.g. Query `limit`)
290
- const e = classifyExpr(node);
291
- switch (e.kind) {
292
- case "str":
293
- case "bool":
294
- case "null":
295
- case "ref":
296
- return true;
297
- case "concat":
298
- return e.parts.every((p) => portIsStatic(reconstructG(p)));
299
- default:
300
- return false;
134
+ catch {
135
+ return false; // an eligibility probe: any lowering failure ⇒ not native-covered here.
301
136
  }
302
137
  }
303
138
  /** The output must lower to a typed struct/value with no Value-scope read: a ref whose head is a
@@ -345,8 +180,8 @@ function isFullyTyped(comp) {
345
180
  }
346
181
  return true;
347
182
  }
348
- function isNativeRaw(comp) {
349
- return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
183
+ function isNativeRaw(comp, plan) {
184
+ return isSequentialComponentRefRead(comp, plan) && isFullyTyped(comp);
350
185
  }
351
186
  /**
352
187
  * coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet). A BATCHED `map` node whose:
@@ -361,7 +196,7 @@ function isNativeRaw(comp) {
361
196
  * element struct by copying the over element's typed fields + materializing the into field from the
362
197
  * aligned batch raw — NO boxed Value on any plane.
363
198
  */
364
- function coveredMapNode(n, bodyIds, comp) {
199
+ function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
365
200
  if (n === null || typeof n !== "object" || !("map" in n))
366
201
  return false;
367
202
  const m = n.map;
@@ -398,11 +233,22 @@ function coveredMapNode(n, bodyIds, comp) {
398
233
  return false;
399
234
  }
400
235
  const ports = m.ports;
236
+ const asBinding = mapAsBinding(comp, n, typedNodes, plan);
401
237
  for (const p of Object.keys(ports))
402
- if (!portIsStatic(ports[p]))
238
+ if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
403
239
  return false;
404
240
  return true;
405
241
  }
242
+ /** the `$as` element binding for a covered map/fanout node's element ports (name + over-element TypeRef),
243
+ * or undefined when the over element type does not resolve (the node is then uncovered upstream). */
244
+ function mapAsBinding(comp, node, typedNodes, plan) {
245
+ try {
246
+ return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
247
+ }
248
+ catch {
249
+ return undefined;
250
+ }
251
+ }
406
252
  /**
407
253
  * coveredFanoutNode (v3) — a FANOUT node (first-class `@refs` id-list fan-out) is covered when:
408
254
  * - over is a static ref to a PRIOR BODY NODE's typed arr field OR an INPUT array port with elemType,
@@ -411,7 +257,7 @@ function coveredMapNode(n, bodyIds, comp) {
411
257
  * The fanout de-boxes to: one deduped batched dispatch → first-seen dedupe (by the elem's dedupeKey
412
258
  * field) + dangling drop + implicitSource strip + connection wrap {items, cursor:nil} — NO boxed Value.
413
259
  */
414
- function coveredFanoutNode(n, bodyIds, comp) {
260
+ function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
415
261
  if (n === null || typeof n !== "object" || !("fanout" in n))
416
262
  return false;
417
263
  const f = n.fanout;
@@ -431,8 +277,15 @@ function coveredFanoutNode(n, bodyIds, comp) {
431
277
  return false;
432
278
  }
433
279
  const ports = f.ports;
280
+ let asBinding;
281
+ try {
282
+ asBinding = { name: f.as, ref: fanoutOverElemInfo(comp, n, typedNodes, plan).elemRef, plan };
283
+ }
284
+ catch {
285
+ asBinding = undefined;
286
+ }
434
287
  for (const p of Object.keys(ports))
435
- if (!portIsStatic(ports[p]))
288
+ if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
436
289
  return false;
437
290
  return true;
438
291
  }
@@ -441,8 +294,8 @@ function coveredFanoutNode(n, bodyIds, comp) {
441
294
  * reads. A real-concurrency plan whose parallel members include a map / bindField-relation child is
442
295
  * NOT covered here (falls through) — the static-parallel orchestration covers the componentRef fan-out
443
296
  * (the GroupAccessRead shape); a parallel map/relation-child stage is a further shape (out of scope). */
444
- function isSequentialComponentRefRead(comp) {
445
- return coverageRejectReason(comp) === null;
297
+ function isSequentialComponentRefRead(comp, plan) {
298
+ return coverageRejectReason(comp, plan) === null;
446
299
  }
447
300
  /**
448
301
  * coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89). Returns
@@ -452,10 +305,11 @@ function isSequentialComponentRefRead(comp) {
452
305
  * never disagree. This is the message the fail-closed dispatch surfaces per non-native component — the
453
306
  * user's signal to add native coverage, fix a genuinely-dynamic spec, or use the --ir dynamic surface.
454
307
  */
455
- function coverageRejectReason(comp) {
308
+ function coverageRejectReason(comp, plan) {
456
309
  const ep = execPlan(comp);
457
310
  if (ep === null)
458
311
  return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
312
+ const typedNodes = buildTypedNodes(comp, plan);
459
313
  // A parallel-stage member must be a componentRef node (no map) so the static parallel orchestration
460
314
  // is a clean fan-out — the bounded-concurrency error-precedence protocol is proven for that shape.
461
315
  // A map child inside a parallel stage still falls through (its per-element loop is a further shape).
@@ -500,7 +354,7 @@ function coverageRejectReason(comp) {
500
354
  if ("fanout" in n) {
501
355
  // v3: a first-class fanout node (deduped connection list-in fan-out) IS covered when its over is a
502
356
  // covered ref (prior-node arr / input array with elemType) and its element ports are static.
503
- if (!coveredFanoutNode(n, bodyIds, comp))
357
+ if (!coveredFanoutNode(n, bodyIds, comp, plan, typedNodes))
504
358
  return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
505
359
  continue;
506
360
  }
@@ -508,7 +362,7 @@ function coverageRejectReason(comp) {
508
362
  // #86 pt2 batched map...into / #93 shape 2/3 / #108 guarded map (no into) / map-over-input (with
509
363
  // elemType) ARE covered. A non-covered map shape (guard+into heterogeneous / over-input without
510
364
  // elemType) falls through to the loud fail-closed reject.
511
- if (!coveredMapNode(n, bodyIds, comp))
365
+ if (!coveredMapNode(n, bodyIds, comp, plan, typedNodes))
512
366
  return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
513
367
  continue;
514
368
  }
@@ -539,7 +393,7 @@ function coverageRejectReason(comp) {
539
393
  if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
540
394
  return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
541
395
  for (const p of Object.keys(cr.ports))
542
- if (portIsStatic(cr.ports[p]) === false)
396
+ if (portIsStatic(cr.ports[p], comp, plan, typedNodes) === false)
543
397
  return `node '${n.id}' port '${p}' is not statically resolvable`;
544
398
  }
545
399
  if (!outputIsNativeLowerable(comp))
@@ -547,21 +401,21 @@ function coverageRejectReason(comp) {
547
401
  return null;
548
402
  }
549
403
  /** Fail closed if a covered read SHAPE lacks the typed annotations (no silent boxed re-box). */
550
- function assertTypedCoverage(comp) {
551
- if (!isSequentialComponentRefRead(comp))
404
+ function assertTypedCoverage(comp, plan) {
405
+ if (!isSequentialComponentRefRead(comp, plan))
552
406
  return;
553
407
  if (isFullyTyped(comp))
554
408
  return;
555
409
  throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#77): component '${comp.name}' is a sequential componentRef read but lacks outType/outputType on every node — the combined emitter de-boxes the RESULT into the node's outType struct, so a covered read MUST be fully typed (bc#45). Add outType annotations, or regenerate on go-straightline-native (boxed result).`);
556
410
  }
557
411
  // ── native ports struct + PortReader ──────────────────────────────────────────────────
558
- function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
412
+ function emitPortsStruct(comp, node, plan, typedNodes, asBinding) {
559
413
  const structName = portsStructName(comp.name, node.id);
560
414
  const portNames = Object.keys(node.ports);
561
415
  if (portNames.length === 0) {
562
416
  return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
563
417
  }
564
- const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding, plan, typedNodes));
418
+ const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp, plan, typedNodes, `port '${p}' of node '${node.id}'`, asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined));
565
419
  const fieldNames = portNames.map(goPortFieldName);
566
420
  const fields = portNames
567
421
  .map((p, i) => `\t${fieldNames[i]} ${fieldTypes[i]} // ${JSON.stringify(p)}`)
@@ -775,7 +629,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
775
629
  // struct element). A pure-static/concat port ignores the binding.
776
630
  const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
777
631
  const asBinding = { name: m.as, ref: elemRef, plan };
778
- const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan);
632
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, plan, typedNodes, asBinding);
779
633
  const batchStruct = `${structName}_batch`;
780
634
  return `${elemStruct}
781
635
 
@@ -793,7 +647,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
793
647
  const structName = portsStructName(comp.name, node.id);
794
648
  const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
795
649
  const asBinding = { name: f.as, ref: elemRef, plan };
796
- const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan);
650
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, plan, typedNodes, asBinding);
797
651
  const batchStruct = `${structName}_batch`;
798
652
  return `${elemStruct}
799
653
 
@@ -940,43 +794,6 @@ function mapOverElemInfo(comp, node, typedNodes, plan) {
940
794
  function mapOverElemType(comp, node, typedNodes, plan) {
941
795
  return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
942
796
  }
943
- /**
944
- * priorNodeArrElemInfo (#129) — resolve a leaf port that is a `ref` into a PRIOR BODY NODE whose
945
- * annotated `outType` is (or reaches, through a field path) a native array, to its element TypeRef +
946
- * the native Go slice expression that yields it. This GENERALIZES the `map…over` prior-node-arr
947
- * dataflow (mapOverElemInfo's first branch) to ANY leaf input port: a fold/dedup leaf whose `items`
948
- * port is `{ref:["<priorNode>"]}` (or `.items` of a prior connection node) now lowers to `[]ElemT`
949
- * fed straight from the prior node's typed local — ZERO boxed Value on the read hot path.
950
- *
951
- * The element type comes STRICTLY from the prior node's EXPLICIT `outType` annotation walked through
952
- * typedFieldAccess (BC does NOT infer — consumer-interface C3 / [[graphddb-typed-native-static-link]]):
953
- * if the head is not a prior body node, or its outType (through the field path) is not an `arr`, this
954
- * returns null and the caller keeps its existing fail-closed behaviour (never a silent box). A
955
- * single-segment INPUT-array port (elemType) is a DIFFERENT covered shape (portArrayElemRef) — handled
956
- * ahead of this. Requires the typedNodes map (prior-node outType TypeRefs) and a TypePlan.
957
- */
958
- function priorNodeArrElemInfo(node, typedNodes, plan) {
959
- if (typedNodes === undefined || plan === undefined)
960
- return null;
961
- if (staticArrElems(node) !== null)
962
- return null; // a static array literal — not a prior-node ref.
963
- const e = classifyExpr(node);
964
- if (e.kind !== "ref" || e.opt || e.path.length < 1)
965
- return null;
966
- if (!typedNodes.has(e.path[0]))
967
- return null; // head is not a prior body node → not this shape.
968
- const baseRef = typedNodes.get(e.path[0]);
969
- let acc;
970
- try {
971
- acc = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
972
- }
973
- catch {
974
- return null; // the field path does not resolve on the prior node's outType → fail-closed at caller.
975
- }
976
- if (acc.ref.kind !== "arr")
977
- return null; // the prior-node result (through the path) is not an array.
978
- return { elemRef: acc.ref.elem, overExpr: acc.expr };
979
- }
980
797
  function classifyPort(node) {
981
798
  if (staticArrElems(node) !== null)
982
799
  return "static";
@@ -1030,43 +847,31 @@ function reconstructG(e) {
1030
847
  function inStructName(compName) {
1031
848
  return `In_${sanitize(compName)}`;
1032
849
  }
1033
- /** Go type for an input port's declared type (scalar → concrete; array+elemType → []ElemT; else Value). */
1034
- function inputPortGoType(schema, plan) {
1035
- switch (schema?.type) {
1036
- case "string":
1037
- // a `"literal"` port is a constrained STRING union (`param.literal(…)`, stored 'S') → native string.
1038
- case "literal":
1039
- return "string";
1040
- case "int":
1041
- return "int64";
1042
- case "float":
1043
- // a `"number"` port is the FLOAT channel (run_behavior: number float / f64 / float64).
1044
- case "number":
1045
- return "float64";
1046
- case "bool":
1047
- return "bool";
1048
- case "array":
1049
- case "arr": {
1050
- // #108: an input ARRAY port with a declared elemType lowers to a native []ElemT (the map-over-input
1051
- // $as element type source). Without elemType or a plan, it stays a boxed Value (uncovered / fail-closed).
1052
- const et = schema.elemType;
1053
- if (et !== undefined && plan !== undefined)
1054
- return `[]${renderTypeRef(deriveTypeRef(et, plan))}`;
1055
- return `${PKG}.Value`;
1056
- }
1057
- case "map": {
1058
- // an input MAP port (dynamic string keys, declared value elemType) lowers to a native map[string]ElemT.
1059
- const et = schema.elemType;
1060
- if (et !== undefined && plan !== undefined)
1061
- return `map[string]${renderTypeRef(deriveTypeRef(et, plan))}`;
1062
- return `${PKG}.Value`;
1063
- }
1064
- default:
1065
- return `${PKG}.Value`;
1066
- }
1067
- }
1068
- /** The scalar kind an input port declares, or undefined if it is not a native scalar. */
850
+ /**
851
+ * inputPortGoType the native Go type for an input port's declared type. Scalars lower to the concrete
852
+ * scalar; `array`/`map` with a declared elemType to `[]ElemT` / `map[string]ElemT`; an OPTIONAL port
853
+ * (`{opt:T}` → `required:false`) to `*T` — the native representation of "the value is absent" (nil).
854
+ * A declared type with no native lowering stays the boxed Value.
855
+ *
856
+ * An OPTIONAL port whose inner type has no native lowering FAILS CLOSED: a boxed `Value` cannot carry
857
+ * the presence distinction on the covered plane, and emitting the bare inner type would silently turn
858
+ * "absent" into a zero value. A port whose optionality cannot be represented is never silently coerced.
859
+ */
860
+ function inputPortGoType(schema, plan, where = "input port") {
861
+ const ref = inputPortTypeRef(schema, plan);
862
+ if (ref !== undefined)
863
+ return renderTypeRef(ref);
864
+ if (inputPortIsOptional(schema))
865
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Go lowering, so "absent" has no native representation (a boxed Value would be an escape, and the bare inner type would silently read absent as a zero value). Declare an elemType for an array/map, or use a natively-lowerable scalar.`);
866
+ return `${PKG}.Value`;
867
+ }
868
+ /** The scalar kind a REQUIRED input port declares, or undefined when it is not a required native scalar.
869
+ * An OPTIONAL port is deliberately NOT a scalar kind here: its native type is `*T`, and the callers of
870
+ * this function build REQUIRED scalar slots (`in.<Field>` read straight into a non-opt port field).
871
+ * Answering "int64" for an `{opt:"int"}` port is exactly how "absent" would silently become `0`. */
1069
872
  function inputScalarKind(schema) {
873
+ if (inputPortIsOptional(schema))
874
+ return undefined;
1070
875
  switch (schema?.type) {
1071
876
  case "string":
1072
877
  case "literal": // a `"literal"` union is a constrained string (see inputPortGoType).
@@ -1091,7 +896,7 @@ function emitInStruct(comp, plan) {
1091
896
  return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
1092
897
  }
1093
898
  const names = ports.map((p) => goFieldName(p));
1094
- const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan));
899
+ const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`));
1095
900
  const nameW = Math.max(0, ...names.map((n) => n.length));
1096
901
  const typeW = Math.max(0, ...types.map((t) => t.length));
1097
902
  const fields = ports
@@ -1132,6 +937,15 @@ function emitInDecoder(comp, plan) {
1132
937
  const scalar = inputScalarKind(schema);
1133
938
  const et = schema.elemType;
1134
939
  lines.push(`\tif v, ok := input.Get(${JSON.stringify(p)}); ok {`);
940
+ if (inputPortIsOptional(schema)) {
941
+ // An OPTIONAL port decodes into its native *T. materializeExpr's opt de-box IS the wire rule:
942
+ // a nil Value (null) → nil (absent), any other value → a pointer to the materialized inner. A key
943
+ // that never appears leaves the struct's zero value (nil) — the same absent value. Checked FIRST:
944
+ // optionality wraps every inner type (an optional array is *[]T, not []T).
945
+ lines.push(`\t\tin.${field} = ${goTypedInternals.materializeExpr("v", inputPortTypeRef(schema, plan), plan)}`);
946
+ lines.push(`\t}`);
947
+ continue;
948
+ }
1135
949
  if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
1136
950
  // #108: decode the boxed Value array into the native []ElemT (test glue — materialize each element
1137
951
  // via the typed materializer; a non-array or non-conforming element is a fail-closed decode error).
@@ -1175,115 +989,17 @@ function emitInDecoder(comp, plan) {
1175
989
  return lines.join("\n");
1176
990
  }
1177
991
  /**
1178
- * emitNativeScalarlower a static port expr to a NATIVE Go expression of the concrete `want` scalar
1179
- * type, or null if it cannot be statically proven that type. Literals: a string literal is native for
1180
- * `want==="string"`, a bool literal for `want==="bool"`. A ref resolves via `resolveRef` (typed input
1181
- * field / prior-node scalar / element binding). A concat lowers ONLY into `string` and only when EVERY
1182
- * part is itself a native string (so the concat's strings-only TYPE_MISMATCH can never fire — the parts
1183
- * are statically strings), emitting `p0 + p1 + …`. Returns null (fall back to the Value path) otherwise.
992
+ * emitPortInitthe port field initializer, read off the ONE port lowering (compilePortNode / native-
993
+ * expr). The field's declared TYPE (portFieldGoType) is the SAME lowering's TypeRef, so type and value
994
+ * can never disagree. `isPrior` tests whether a ref head names a settled prior node at this position;
995
+ * `opts.asBinding`/`opts.asBase` carry a map/fanout element binding. A FALLIBLE lowering (hoisted
996
+ * statements) is rejected HERE a ports-struct literal cannot host statements.
1184
997
  */
1185
- function emitNativeScalar(node, want, resolveRef) {
1186
- // a static arr is a Value port (never a scalar) — not native-lowerable here.
1187
- if (staticArrElems(node) !== null)
1188
- return null;
1189
- const e = classifyExpr(node);
1190
- switch (e.kind) {
1191
- case "str":
1192
- return want === "string" ? JSON.stringify(e.value) : null;
1193
- case "bool":
1194
- return want === "bool" ? (e.value ? "true" : "false") : null;
1195
- case "null":
1196
- return null; // a null literal is a Value(nil), never a required native scalar
1197
- case "ref": {
1198
- const sc = resolveRef(e.path[0], e.path.slice(1), e.opt);
1199
- if (sc === null || sc.scalar !== want)
1200
- return null;
1201
- return sc.expr;
1202
- }
1203
- case "concat": {
1204
- if (want !== "string")
1205
- return null;
1206
- const parts = [];
1207
- for (const p of e.parts) {
1208
- const sub = emitNativeScalar(reconstructG(p), "string", resolveRef);
1209
- if (sub === null)
1210
- return null;
1211
- // parenthesize a `+` sub-expression so string concatenation binds correctly (a ref/literal is
1212
- // atomic; a nested concat is already a single `a + b` — wrap to keep it one operand).
1213
- parts.push(sub.includes(" + ") ? `(${sub})` : sub);
1214
- }
1215
- return parts.join(" + ");
1216
- }
1217
- default:
1218
- return null;
1219
- }
1220
- }
1221
- /**
1222
- * emitPortInit — emit the port field build for one port as a FULLY NATIVE Go expression (bc#90 /
1223
- * runtime-free). Every covered port lowers to a concrete Go value assigned straight into the ports
1224
- * struct field — NO `dslcontracts.Value`, NO nvVE helper, NO `.(T)` type-assert:
1225
- * - a static string array (projection) → `[]string{"a", "b", ...}` (change #2);
1226
- * - a bare int/float number literal (e.g. Query `limit: 20`) → `int64(20)` / `float64(...)`;
1227
- * - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
1228
- * A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered
1229
- * plane — a boxed escape would re-introduce the bc-runtime coupling this module removes). Returns the
1230
- * struct field initializer (`Field: <expr>`). `resolveRef` resolves a ref head for the scalar path.
1231
- */
1232
- function emitPortInit(pn, expr, fieldGoType, resolveRef, inputPorts, plan, typedNodes, asBinding, elemBaseExpr) {
1233
- const fieldName = goPortFieldName(pn);
1234
- // #108-map: a port that is a ref to a `$as` element MAP/NAMED field (or an input map port) → the typed
1235
- // composite value directly off the element/input (native map[string]V / struct — ZERO boxed Value).
1236
- if (plan !== undefined) {
1237
- const compExpr = emitCompositePortValue(expr, inputPorts ?? {}, asBinding, plan, elemBaseExpr);
1238
- if (compExpr !== null)
1239
- return `${fieldName}: ${compExpr}`;
1240
- }
1241
- // #110: a componentRef port bound to a runtime array input port (with elemType) → the native []ElemT,
1242
- // read straight from the concrete input struct (`in.<Field>`) — ZERO boxed Value on the read hot path.
1243
- // The over-elem/element-source case (map…over) already resolves via mapOverElemInfo; this is the plain
1244
- // componentRef port (an IN-list / array-bound WHERE head).
1245
- if (inputPorts !== undefined && portArrayElemRef(expr, inputPorts, plan) !== null) {
1246
- const e = classifyExpr(expr);
1247
- if (e.kind === "ref" && e.path.length === 1) {
1248
- return `${fieldName}: in.${goFieldName(e.path[0])}`;
1249
- }
1250
- }
1251
- // #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) →
1252
- // the native []ElemT read straight off the prior node's typed local (`t_<node>.Field…`) — ZERO boxed
1253
- // Value on the read hot path. The over-source case (map…over) resolves via mapOverElemInfo; this is
1254
- // the plain leaf array input port (a fold/dedup leaf whose `items` = a prior node's array result).
1255
- const priorArr = priorNodeArrElemInfo(expr, typedNodes, plan);
1256
- if (priorArr !== null)
1257
- return `${fieldName}: ${priorArr.overExpr}`;
1258
- // change #2: a static string array (projection) → a native `[]string{...}` literal.
1259
- const strArr = staticStringArrElems(expr);
1260
- if (strArr !== null) {
1261
- if (fieldGoType !== "[]string") {
1262
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90): port '${pn}' is a static string array but its field type is '${fieldGoType}' (expected []string).`);
1263
- }
1264
- return `${fieldName}: []string{${strArr.map((s) => JSON.stringify(s)).join(", ")}}`;
1265
- }
1266
- // a bare number literal (e.g. Query `limit: 20`): the SSoT range check ran in numLiteralGoExpr —
1267
- // emit the concrete native int64/float64 (unwrap the dslcontracts.Value(...) box: it was only there
1268
- // for the old boxed path; the range check is what matters, and it already passed).
1269
- const num = numLiteralGoExpr(expr);
1270
- if (num !== null) {
1271
- if (fieldGoType === "int64")
1272
- return `${fieldName}: int64(${expr})`;
1273
- if (fieldGoType === "float64")
1274
- return `${fieldName}: float64(${expr})`;
1275
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90): port '${pn}' is a number literal but its field type is '${fieldGoType}'.`);
1276
- }
1277
- // string / bool / scalar-input / concat → native scalar expression (no Value, no `.(T)`).
1278
- const scalarWant = fieldGoType;
1279
- const native = emitNativeScalar(expr, scalarWant, resolveRef);
1280
- if (native !== null) {
1281
- return `${fieldName}: ${native}`;
1282
- }
1283
- // FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would
1284
- // re-couple the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the
1285
- // component is not native-eligible and must regenerate on the boxed straight-line path.
1286
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): port '${pn}' (${JSON.stringify(expr)}) does not lower to a native Go value of type '${fieldGoType}'. The covered read plane admits NO boxed dslcontracts.Value escape — regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
998
+ function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
999
+ const c = compilePortNode(expr, comp, plan, typedNodes, isPrior, `port '${pn}'`, opts);
1000
+ if (c.stmts.length > 0)
1001
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): port '${pn}' ${JSON.stringify(expr)} lowers to a FALLIBLE expression (one that can raise INT_OVERFLOW / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS). Such an expression lowers to STATEMENTS carrying an early error-return, and a port is built inline in the ports-struct literal — a position that cannot host them. (A cond/guard position CAN host them, so the same expression is covered there — the boundary is the POSITION, not the operator.) Use a non-fallible port expression.`);
1002
+ return `${goPortFieldName(pn)}: ${c.expr}`;
1287
1003
  }
1288
1004
  /**
1289
1005
  * emitParallelStageArm (bc#87) — EXPLICIT static parallel orchestration for a real-concurrency stage.
@@ -1306,7 +1022,7 @@ function emitPortInit(pn, expr, fieldGoType, resolveRef, inputPorts, plan, typed
1306
1022
  *
1307
1023
  * NO RunPlan / []OpSpec / planSpec walk: the staging, the bound, and which ops are parallel are baked.
1308
1024
  */
1309
- function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags, resolveRefAt) {
1025
+ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags) {
1310
1026
  const lines = [];
1311
1027
  const ids = stage.map((i) => comp.body[i].id);
1312
1028
  lines.push(`\t// ── bc#87 real-concurrency stage {${ids.map((x) => JSON.stringify(x)).join(", ")}} — static parallel`);
@@ -1395,11 +1111,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1395
1111
  }
1396
1112
  }
1397
1113
  const inits = [];
1398
- const resolveRef = resolveRefAt(atPos);
1399
1114
  for (const pn of Object.keys(node.ports)) {
1400
- const expr = node.ports[pn];
1401
- const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
1402
- inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef, comp.inputPorts, plan, typedNodes));
1115
+ inits.push(emitPortInit(pn, node.ports[pn], comp, plan, typedNodes, (h) => priorNodeAt(h, atPos)));
1403
1116
  }
1404
1117
  lines.push(`${ind}ps_${s} = ${structName}{${inits.join(", ")}}`);
1405
1118
  lines.push(`${ind}run_${s} = true`);
@@ -1506,35 +1219,6 @@ function emitNativeRawRunner(comp, plan, flags) {
1506
1219
  // assert on a row value — the runner reads row.<Field> directly and copies each into the outType local.
1507
1220
  lines.push(`func ${runnerName(comp.name)}[H ${handlerIfaceName(comp.name)}](handlers H, in ${inStructName(comp.name)}) (${outStructType}, error) {`);
1508
1221
  lines.push(`\t_ = in`);
1509
- // resolveNativeRef for this runner: a ref head is either a PRIOR NODE (read its typed struct field
1510
- // directly) or an INPUT PORT (read the typed `in.<Field>`). A REQUIRED scalar resolves to a native
1511
- // Go expression; an opt / non-scalar / refOpt resolves to null (falls back to the nvVE Value path).
1512
- const resolveRefAt = (atPos) => (head, restPath, opt) => {
1513
- if (opt)
1514
- return null; // a refOpt may be nil at runtime — keep the Value path (opt-nil semantics)
1515
- if (priorNodeAt(head, atPos)) {
1516
- const baseRef = typedNodes.get(head);
1517
- let acc;
1518
- let expr;
1519
- try {
1520
- const r = goTypedInternals.typedFieldAccess(typedLocal(head), baseRef, restPath, plan);
1521
- expr = r.expr;
1522
- acc = r.ref;
1523
- }
1524
- catch {
1525
- return null;
1526
- }
1527
- if (acc.kind !== "scalar" || acc.scalar === "null")
1528
- return null;
1529
- return { expr, scalar: goScalar(acc.scalar) };
1530
- }
1531
- if (restPath.length !== 0)
1532
- return null; // input.<field> path is a Value walk — not a native scalar
1533
- const scalar = inputScalarKind(comp.inputPorts?.[head]);
1534
- if (scalar === undefined)
1535
- return null;
1536
- return { expr: `in.${goFieldName(head)}`, scalar };
1537
- };
1538
1222
  for (const k of order.keys()) {
1539
1223
  const node = comp.body[order[k]];
1540
1224
  const id = node.id;
@@ -1557,7 +1241,7 @@ function emitNativeRawRunner(comp, plan, flags) {
1557
1241
  // ascending-sorted stages), so we emit the block at the first member and skip the rest.
1558
1242
  const stage = ep.parallelStageOf.get(i);
1559
1243
  if (stage !== undefined && stage[0] === i) {
1560
- lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags, resolveRefAt));
1244
+ lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags));
1561
1245
  k += stage.length - 1;
1562
1246
  continue;
1563
1247
  }
@@ -1629,11 +1313,8 @@ function emitNativeRawRunner(comp, plan, flags) {
1629
1313
  // dslcontracts.Value, ZERO nvVE helper, ZERO `.(T)`. A genuinely-dynamic port FAILS CLOSED.
1630
1314
  const inits = [];
1631
1315
  const boundArg = node.bindField !== undefined ? `bound_${s}` : "nil";
1632
- const resolveRef = resolveRefAt(k);
1633
1316
  for (const pn of portNames) {
1634
- const expr = node.ports[pn];
1635
- const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
1636
- inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef, comp.inputPorts, plan, typedNodes));
1317
+ inits.push(emitPortInit(pn, node.ports[pn], comp, plan, typedNodes, (h) => priorNodeAt(h, k)));
1637
1318
  }
1638
1319
  lines.push(`${indent}ports_${s} := ${structName}{${inits.join(", ")}}`);
1639
1320
  const oc = `row_${s}`;
@@ -1726,67 +1407,15 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1726
1407
  const mkGuard = (elemVar) => {
1727
1408
  if (!hasGuard)
1728
1409
  return null;
1729
- const resolveHead = (head) => {
1730
- if (head === asName)
1731
- return { expr: elemVar, ref: overElemRef };
1732
- if (priorHere(head))
1733
- return { expr: typedLocal(head), ref: typedNodes.get(head) };
1734
- return goInputHeadRef(head, comp, plan);
1735
- };
1736
- const be = makeGoExprBackend(zeroOut, resolveHead, plan);
1737
- return new NativeExprCompiler(be).compileBool(m.when);
1410
+ const resolveHead = goNativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding: { name: asName, ref: overElemRef, plan }, asBase: elemVar });
1411
+ return new NativeExprCompiler(makeGoExprBackend(zeroOut, resolveHead, plan)).compileBool(m.when);
1738
1412
  };
1739
1413
  // emit the per-element native ports struct build (shared; `il` is the indent inside the element
1740
1414
  // loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
1741
1415
  const buildPorts = (il, elemVar) => {
1742
- const out = [];
1743
- const inits = [];
1744
- // resolveNativeRef for a map element port: a ref head is the element binding ($as → the over element
1745
- // typed struct field `elemVar.<Field>`), a PRIOR NODE (its typed struct field), or an INPUT PORT
1746
- // (`in.<Field>`). A REQUIRED scalar lowers to a native Go expr; opt / non-scalar → null (Value path).
1747
- const resolveRef = (head, restPath, opt) => {
1748
- if (opt)
1749
- return null;
1750
- let baseExpr;
1751
- let baseRef;
1752
- if (head === asName) {
1753
- baseExpr = elemVar;
1754
- baseRef = overElemRef;
1755
- }
1756
- else if (priorHere(head)) {
1757
- baseExpr = typedLocal(head);
1758
- baseRef = typedNodes.get(head);
1759
- }
1760
- else {
1761
- if (restPath.length !== 0)
1762
- return null;
1763
- const scalar = inputScalarKind(comp.inputPorts?.[head]);
1764
- if (scalar === undefined)
1765
- return null;
1766
- return { expr: `in.${goFieldName(head)}`, scalar };
1767
- }
1768
- let acc;
1769
- let expr;
1770
- try {
1771
- const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
1772
- expr = r.expr;
1773
- acc = r.ref;
1774
- }
1775
- catch {
1776
- return null;
1777
- }
1778
- if (acc.kind !== "scalar" || acc.scalar === "null")
1779
- return null;
1780
- return { expr, scalar: goScalar(acc.scalar) };
1781
- };
1782
1416
  const asBinding = { name: asName, ref: overElemRef, plan };
1783
- for (const pn of portNames) {
1784
- const fieldGoType = portFieldGoType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding, plan);
1785
- // #108-map: pass the element binding + base expr so a `$as` MAP/NAMED field port reads the typed composite.
1786
- inits.push(emitPortInit(pn, m.ports[pn], fieldGoType, resolveRef, comp.inputPorts, plan, undefined, asBinding, elemLocal(s)));
1787
- }
1788
- out.push(`${il}ep_${s} := ${structName}{${inits.join(", ")}}`);
1789
- return out;
1417
+ const inits = portNames.map((pn) => emitPortInit(pn, m.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: elemVar }));
1418
+ return [`${il}ep_${s} := ${structName}{${inits.join(", ")}}`];
1790
1419
  };
1791
1420
  const lines = [];
1792
1421
  const closers = [];
@@ -1924,47 +1553,8 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
1924
1553
  const dkGo = goFieldName(f.dedupeKey);
1925
1554
  // per-id native ports build (same as a batched map's per-element ports). $as → the over id value.
1926
1555
  const buildPorts = (il, elemVar) => {
1927
- const inits = [];
1928
- const resolveRef = (head, restPath, opt) => {
1929
- if (opt)
1930
- return null;
1931
- let baseExpr;
1932
- let baseRef;
1933
- if (head === asName) {
1934
- baseExpr = elemVar;
1935
- baseRef = overElemRef;
1936
- }
1937
- else if (priorHere(head)) {
1938
- baseExpr = typedLocal(head);
1939
- baseRef = typedNodes.get(head);
1940
- }
1941
- else {
1942
- if (restPath.length !== 0)
1943
- return null;
1944
- const scalar = inputScalarKind(comp.inputPorts?.[head]);
1945
- if (scalar === undefined)
1946
- return null;
1947
- return { expr: `in.${goFieldName(head)}`, scalar };
1948
- }
1949
- let acc;
1950
- let expr;
1951
- try {
1952
- const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
1953
- expr = r.expr;
1954
- acc = r.ref;
1955
- }
1956
- catch {
1957
- return null;
1958
- }
1959
- if (acc.kind !== "scalar" || acc.scalar === "null")
1960
- return null;
1961
- return { expr, scalar: goScalar(acc.scalar) };
1962
- };
1963
1556
  const asBinding = { name: asName, ref: overElemRef, plan };
1964
- for (const pn of portNames) {
1965
- const fieldGoType = portFieldGoType(f.ports[pn], comp.inputPorts, `fanout port '${pn}' of node '${node.id}'`, asBinding, plan);
1966
- inits.push(emitPortInit(pn, f.ports[pn], fieldGoType, resolveRef, comp.inputPorts, plan));
1967
- }
1557
+ const inits = portNames.map((pn) => emitPortInit(pn, f.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: elemVar }));
1968
1558
  return [`${il}ep_${s} := ${structName}{${inits.join(", ")}}`];
1969
1559
  };
1970
1560
  const lines = [];
@@ -2019,23 +1609,19 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
2019
1609
  lines.push(c);
2020
1610
  return lines.join("\n");
2021
1611
  }
2022
- /** #108: resolve an INPUT port head for a native-expr ref (cond `if` / map `when` / branches). A scalar
2023
- * port `in.<Field>` of the scalar type; an array port WITH elemType `in.<Field>` of `[]ElemT` (so
2024
- * `len(in.<Field>)` works); anything else null (fail-closed). */
1612
+ /**
1613
+ * goInputHeadRef (#108) resolve an INPUT port head for a native-expr ref (cond `if` / map `when` /
1614
+ * branches / an opt-lane port). The port's declared type comes from the shared `inputPortTypeRef` SSoT,
1615
+ * so an OPTIONAL port resolves as `{opt:…}` (native `*T`) and the compiler can see its optionality: a
1616
+ * null-compare becomes a real presence test and a coalesce a real deref-or-default, while a
1617
+ * REQUIRED-scalar slot fed by an opt fails closed. Go values are read directly off the input struct (no
1618
+ * ownership ceremony); a type with no native lowering → null (fail-closed).
1619
+ */
2025
1620
  function goInputHeadRef(head, comp, plan) {
2026
- const schema = comp.inputPorts?.[head];
2027
- if (schema === undefined)
1621
+ const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
1622
+ if (ref === undefined)
2028
1623
  return null;
2029
- const sc = inputScalarKind(schema);
2030
- if (sc !== undefined) {
2031
- const scalarRef = { kind: "scalar", scalar: sc === "int64" ? "int" : sc === "float64" ? "float" : sc };
2032
- return { expr: `in.${goFieldName(head)}`, ref: scalarRef };
2033
- }
2034
- const et = schema.elemType;
2035
- if ((schema.type === "array" || schema.type === "arr") && et !== undefined) {
2036
- return { expr: `in.${goFieldName(head)}`, ref: { kind: "arr", elem: deriveTypeRef(et, plan) } };
2037
- }
2038
- return null;
1624
+ return { expr: `in.${goFieldName(head)}`, ref };
2039
1625
  }
2040
1626
  /**
2041
1627
  * emitCondArm — the struct-native exec of a covered cond node (#108). Mirrors run_behavior's cond
@@ -2049,11 +1635,7 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut)
2049
1635
  const c = node.cond;
2050
1636
  const s = sanitize(node.id);
2051
1637
  const priorHere = (head) => priorNodeAt(head, atPos);
2052
- const resolveHead = (head) => {
2053
- if (priorHere(head))
2054
- return { expr: typedLocal(head), ref: typedNodes.get(head) };
2055
- return goInputHeadRef(head, comp, plan);
2056
- };
1638
+ const resolveHead = goNativeResolveHead(comp, plan, typedNodes, priorHere);
2057
1639
  const be = makeGoExprBackend(zeroOut, resolveHead, plan);
2058
1640
  const compiler = new NativeExprCompiler(be);
2059
1641
  const cond = compiler.compileBool(c.if);
@@ -2276,11 +1858,31 @@ function makeGoExprBackend(zeroOut, resolveHead, plan) {
2276
1858
  notOp: (a) => `(!${a})`,
2277
1859
  structLit: (name, inits) => `${name}{${inits.map((i) => `${goFieldName(i.field)}: ${i.expr}`).join(", ")}}`,
2278
1860
  arrLit: (elemTy, items) => `[]${elemTy}{${items.join(", ")}}`,
1861
+ // a projection port of static string literals — bare `[]string{"a", "b"}` (Go string literals are
1862
+ // already allocation-free; no divergence from the general arr path, so no golden churn).
1863
+ staticStrArr: (literals) => ({ expr: `[]string{${literals.map((s) => JSON.stringify(s)).join(", ")}}`, type: "[]string" }),
2279
1864
  // an opt (`*T`) NONE literal is `nil` (the inner type is not needed for the untyped nil in Go).
2280
1865
  optNone: (_innerTy) => "nil",
2281
1866
  // opt (`*T`) null-presence test: present = `(expr != nil)`, absent = `(expr == nil)`.
2282
1867
  optIsSome: (expr) => `(${expr} != nil)`,
2283
1868
  optIsNone: (expr) => `(${expr} == nil)`,
1869
+ // opt (`*T`) defaulted to a PURE default. Go has no `unwrap_or`, so this is the same immediately-
1870
+ // invoked func literal the `ternary` rendering uses (the established shape in this emitter): bind the
1871
+ // pointer ONCE, deref it when present, else yield the default. The default sits in the else branch,
1872
+ // so it is evaluated only when absent — run_behavior's lazy right side exactly.
1873
+ optUnwrapOr: (optExpr, defaultExpr, innerTy) => `func() ${innerTy} { if v := ${optExpr}; v != nil { return *v }; return ${defaultExpr} }()`,
1874
+ // opt defaulted to a FALLIBLE default — a statement if/else, NOT a func literal: the default's
1875
+ // hoisted `return zeroOut, err` early returns must leave the ENCLOSING fn, which a func literal
1876
+ // would swallow (it would return from the literal). Only the nil branch evaluates the default.
1877
+ optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
1878
+ `var ${temp} ${ty}`,
1879
+ `if ${bindTemp} := ${optExpr}; ${bindTemp} != nil {`,
1880
+ ` ${temp} = *${bindTemp}`,
1881
+ `} else {`,
1882
+ ...dStmts.map((x) => ` ${x}`),
1883
+ ` ${temp} = ${dExpr}`,
1884
+ `}`,
1885
+ ],
2284
1886
  ternary: (cond, t, e, ty) => `func() ${ty} { if ${cond} { return ${t} }; return ${e} }()`,
2285
1887
  shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
2286
1888
  // and: temp=false; if left { <rStmts>; temp=rExpr }. or: temp=true; if !left { <rStmts>; temp=rExpr }.
@@ -2461,12 +2063,14 @@ var ExpectedSpecVersions = map[string]int64{"behavior": ${sv.behavior}, "express
2461
2063
  }
2462
2064
  // ── module assembly ──────────────────────────────────────────────────────────────────
2463
2065
  function emit(ctx) {
2066
+ // The type plan is the SSoT the coverage gate + the port lowering both read (the gate ASKS the port
2067
+ // lowering whether a port lowers), so it is built up-front — before the coverage checks.
2068
+ const plan = buildTypePlan(ctx.ir);
2464
2069
  // Fail closed if a covered read SHAPE lacks the typed annotations (no silent re-box).
2465
2070
  for (const c of ctx.ir.components)
2466
- assertTypedCoverage(c);
2467
- const native = ctx.ir.components.filter(isNativeRaw);
2468
- const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
2469
- const plan = buildTypePlan(ctx.ir);
2071
+ assertTypedCoverage(c, plan);
2072
+ const native = ctx.ir.components.filter((c) => isNativeRaw(c, plan));
2073
+ const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c, plan));
2470
2074
  const decls = emitTypeDecls(plan);
2471
2075
  // #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler when it emits
2472
2076
  // a fallible checked-arith helper call — determines whether the helper library + `math` import ship).
@@ -2481,11 +2085,7 @@ function emit(ctx) {
2481
2085
  const runnerCodes = [];
2482
2086
  for (const c of native) {
2483
2087
  inStructs.push(emitInStruct(c, plan));
2484
- const typedNodes = new Map();
2485
- // build the full typed-node map FIRST so a map's over-element resolution can see every node.
2486
- for (const n of c.body)
2487
- if (!isControlGate(n))
2488
- typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan)); // #125: control-gate typeless; map は produced-array ref に正規化
2088
+ const typedNodes = buildTypedNodes(c, plan);
2489
2089
  for (const n of c.body) {
2490
2090
  if ("fanout" in n) {
2491
2091
  // v3: a fanout node emits a per-id element ports struct + a batch struct (like a batched map).
@@ -2501,7 +2101,7 @@ function emit(ctx) {
2501
2101
  }
2502
2102
  else {
2503
2103
  // #129: pass typedNodes so a leaf array port fed by a prior node's arr outType types as []ElemT.
2504
- structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes));
2104
+ structs.push(emitPortsStruct(c, n, plan, typedNodes));
2505
2105
  }
2506
2106
  }
2507
2107
  // CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
@@ -2530,7 +2130,7 @@ function emit(ctx) {
2530
2130
  // minimal, runtime-free header.
2531
2131
  if (nonNative.length > 0) {
2532
2132
  const rejects = nonNative
2533
- .map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
2133
+ .map((c) => ` - component '${c.name}': ${coverageRejectReason(c, plan) ?? "not a covered native read"}`)
2534
2134
  .join("\n");
2535
2135
  throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (native codegen) surface: ${nonNative.length} component(s) are NOT a covered native read:\n${rejects}\n\n` +
2536
2136
  `The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
@@ -2588,8 +2188,8 @@ function emit(ctx) {
2588
2188
  */
2589
2189
  export function goTypedNativeObserve(ir, runtimeImport) {
2590
2190
  const components = ir.components;
2591
- const native = components.filter(isNativeRaw);
2592
2191
  const plan = buildTypePlan(ir);
2192
+ const native = components.filter((c) => isNativeRaw(c, plan));
2593
2193
  const serializers = emitSerializers(plan);
2594
2194
  const marshallers = goTypedInternals.emitMarshallers(plan);
2595
2195
  const mustHelpers = emitMustHelpers(plan);
@@ -2713,17 +2313,25 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
2713
2313
  const rowT = rawRowStructName(c.name, node.id);
2714
2314
  const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
2715
2315
  const nodeBoundT = boundGoType(node, c, typedNodes, plan);
2716
- // #129 (TEST glue): if this node has an `items` port and its outType is a bare arr, support the
2717
- // reference scriptedHandlers' `echo === "items"` the scripted handler ECHOES the NATIVE `items`
2718
- // port back as its result. This makes the node→leaf array dataflow test non-vacuous: the leaf's
2719
- // observed result is exactly the native array that flowed into its ports struct, so a dropped or
2720
- // boxed native array would diverge from run_behavior. `ports` is named only when the echo path
2721
- // uses it (else the param stays `_` so a non-echo node compiles clean / unused-param-free).
2722
- const canEcho = ref.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items");
2723
- const portsParam = canEcho ? "ports" : "_";
2724
- const echoBranch = canEcho
2725
- ? `\tif src.echo == "items" {\n\t\treturn ${rowT}{Val: ports.${goPortFieldName("items")}}, true\n\t}\n`
2726
- : "";
2316
+ // #129 (TEST glue): support the reference scriptedHandlers' echo forms (PROTOCOL.md §3.5) the
2317
+ // adapter normalizes `{"echo":"port","port":"X"}` / `{"echo":"items"}` to the port NAME, and the
2318
+ // scripted handler
2319
+ // ECHOES a NATIVE port back as its result. This makes a port-plane pin non-vacuous: the node's
2320
+ // observed result IS the native value that flowed into its ports struct, so a dropped / re-boxed /
2321
+ // zero-defaulted port diverges from run_behavior instead of passing. A port is echoable when its
2322
+ // native type IS the node's outType (that is exactly when the row's Val can hold it); `ports` is
2323
+ // named only when at least one port qualifies (else the param stays `_` so a non-echo node
2324
+ // compiles clean / unused-param-free).
2325
+ const echoable = echoablePorts(c, node, ref, plan, typedNodes);
2326
+ const portsParam = echoable.length > 0 ? "ports" : "_";
2327
+ // a NAMED outType row is FLATTENED (no Val) — echo a named port by copying the struct's fields;
2328
+ // a scalar/arr/opt outType row holds the port in its single Val field.
2329
+ const echoRow = (ep) => ref.kind === "named"
2330
+ ? `${rowT}{${findDecl(plan, ref.name).fields.map((f) => `${goFieldName(f.name)}: ports.${goPortFieldName(ep)}.${goFieldName(f.name)}`).join(", ")}}`
2331
+ : `${rowT}{Val: ports.${goPortFieldName(ep)}}`;
2332
+ const echoBranch = echoable
2333
+ .map((ep) => `\tif src.echo == ${JSON.stringify(ep)} {\n\t\treturn ${echoRow(ep)}, true\n\t}\n`)
2334
+ .join("");
2727
2335
  methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, node.id)}(${portsParam} ${portsStructName(c.name, node.id)}, _ ${nodeBoundT}) (${rowT}, bool) {
2728
2336
  src, ok := s.next(${JSON.stringify(node.component)})
2729
2337
  if !ok {
@@ -2775,10 +2383,19 @@ func NewScriptedNativeRaw(spec *${PKG}.JObj) (*ScriptedNativeRaw, error) {
2775
2383
  for _, e := range entries {
2776
2384
  eo, _ := e.(*${PKG}.JObj)
2777
2385
  // #129: an \`echo\` directive names an input port to echo back (mirrors the reference
2778
- // scriptedHandlers' \`echo === "items"\`). It is drained together with the ok payload.
2386
+ // scriptedHandlers' PROTOCOL.md §3.5 echo forms). It is drained together with the ok
2387
+ // payload. \`{"echo":"port","port":"X"}\` is normalized to the port NAME here, so the
2388
+ // generated node method just compares \`src.echo\` against its echoable port names —
2389
+ // \`{"echo":"items"}\` is already that shape.
2779
2390
  echo := ""
2780
2391
  if echoNode, has := eo.Get("echo"); has {
2781
2392
  echo, _ = echoNode.(string)
2393
+ if echo == "port" {
2394
+ echo = ""
2395
+ if portNode, hasPort := eo.Get("port"); hasPort {
2396
+ echo, _ = portNode.(string)
2397
+ }
2398
+ }
2782
2399
  }
2783
2400
  if okNode, has := eo.Get("ok"); has {
2784
2401
  val, derr := ${PKG}.DecodeValue(okNode)
@@ -2862,6 +2479,14 @@ ${arms}
2862
2479
  }
2863
2480
  `;
2864
2481
  }
2482
+ /** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
2483
+ * declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row (a scalar/arr/
2484
+ * opt row via its Val; a NAMED row via its flattened fields copied from the struct port). A port with a
2485
+ * different type is not echoable (the row could not hold it). */
2486
+ function echoablePorts(comp, node, ref, plan, typedNodes) {
2487
+ const want = renderTypeRef(ref);
2488
+ return Object.keys(node.ports).filter((p) => portFieldGoType(node.ports[p], comp, plan, typedNodes, `port '${p}'`) === want);
2489
+ }
2865
2490
  /** emitDecodeRow — emit (once, deduped) a decoder `decodeRow_<comp>_<node><suffix>(v Value) <rowT>`
2866
2491
  * that materializes the scripted ok Value into the concrete row struct (typed fields = the outType /
2867
2492
  * elem-row type). Named: reuse the Value->struct marshaller + copy fields; scalar/arr/opt: materialize