behavior-contracts 0.5.0 → 0.7.0

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 (112) hide show
  1. package/dist/behavior.d.ts +7 -0
  2. package/dist/behavior.d.ts.map +1 -1
  3. package/dist/behavior.js.map +1 -1
  4. package/dist/builder.d.ts.map +1 -1
  5. package/dist/builder.js +10 -3
  6. package/dist/builder.js.map +1 -1
  7. package/dist/generator/async-plan.d.ts +78 -0
  8. package/dist/generator/async-plan.d.ts.map +1 -0
  9. package/dist/generator/async-plan.js +116 -0
  10. package/dist/generator/async-plan.js.map +1 -0
  11. package/dist/generator/core.d.ts +30 -9
  12. package/dist/generator/core.d.ts.map +1 -1
  13. package/dist/generator/core.js.map +1 -1
  14. package/dist/generator/{emit-straightline-typed-go.d.ts → emit-shared-go-typed.d.ts} +11 -17
  15. package/dist/generator/emit-shared-go-typed.d.ts.map +1 -0
  16. package/dist/generator/emit-shared-go-typed.js +572 -0
  17. package/dist/generator/emit-shared-go-typed.js.map +1 -0
  18. package/dist/generator/emit-shared-go.d.ts +29 -0
  19. package/dist/generator/emit-shared-go.d.ts.map +1 -0
  20. package/dist/generator/{emit-straightline-go.js → emit-shared-go.js} +2 -34
  21. package/dist/generator/emit-shared-go.js.map +1 -0
  22. package/dist/generator/{emit-straightline-typed-rust.d.ts → emit-shared-rust-typed.d.ts} +11 -17
  23. package/dist/generator/emit-shared-rust-typed.d.ts.map +1 -0
  24. package/dist/generator/emit-shared-rust-typed.js +576 -0
  25. package/dist/generator/emit-shared-rust-typed.js.map +1 -0
  26. package/dist/generator/{emit-straightline-rust.d.ts → emit-shared-rust.d.ts} +1 -36
  27. package/dist/generator/emit-shared-rust.d.ts.map +1 -0
  28. package/dist/generator/{emit-straightline-rust.js → emit-shared-rust.js} +2 -16
  29. package/dist/generator/emit-shared-rust.js.map +1 -0
  30. package/dist/generator/emit-shared-typescript.d.ts +12 -0
  31. package/dist/generator/emit-shared-typescript.d.ts.map +1 -0
  32. package/dist/generator/{emit-straightline-typescript.js → emit-shared-typescript.js} +5 -20
  33. package/dist/generator/emit-shared-typescript.js.map +1 -0
  34. package/dist/generator/emit-straightline-typed-typescript.d.ts +1 -1
  35. package/dist/generator/emit-straightline-typed-typescript.d.ts.map +1 -1
  36. package/dist/generator/emit-straightline-typed-typescript.js +1 -1
  37. package/dist/generator/emit-straightline-typed-typescript.js.map +1 -1
  38. package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
  39. package/dist/generator/emit-typed-native-go.js +546 -82
  40. package/dist/generator/emit-typed-native-go.js.map +1 -1
  41. package/dist/generator/emit-typed-native-rust.d.ts +2 -1
  42. package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
  43. package/dist/generator/emit-typed-native-rust.js +597 -110
  44. package/dist/generator/emit-typed-native-rust.js.map +1 -1
  45. package/dist/generator/index.d.ts +2 -12
  46. package/dist/generator/index.d.ts.map +1 -1
  47. package/dist/generator/index.js +10 -62
  48. package/dist/generator/index.js.map +1 -1
  49. package/dist/generator/native-expr.d.ts +120 -0
  50. package/dist/generator/native-expr.d.ts.map +1 -0
  51. package/dist/generator/native-expr.js +456 -0
  52. package/dist/generator/native-expr.js.map +1 -0
  53. package/dist/generator/typed.d.ts.map +1 -1
  54. package/dist/generator/typed.js +8 -0
  55. package/dist/generator/typed.js.map +1 -1
  56. package/dist/guard.js +14 -0
  57. package/dist/guard.js.map +1 -1
  58. package/dist/index.d.ts +2 -2
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +1 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/plan.d.ts +6 -2
  63. package/dist/plan.d.ts.map +1 -1
  64. package/dist/plan.js +4 -0
  65. package/dist/plan.js.map +1 -1
  66. package/package.json +1 -1
  67. package/dist/generator/emit-go.d.ts +0 -19
  68. package/dist/generator/emit-go.d.ts.map +0 -1
  69. package/dist/generator/emit-go.js +0 -79
  70. package/dist/generator/emit-go.js.map +0 -1
  71. package/dist/generator/emit-raw-abi-go.d.ts +0 -45
  72. package/dist/generator/emit-raw-abi-go.d.ts.map +0 -1
  73. package/dist/generator/emit-raw-abi-go.js +0 -346
  74. package/dist/generator/emit-raw-abi-go.js.map +0 -1
  75. package/dist/generator/emit-raw-abi-rust.d.ts +0 -45
  76. package/dist/generator/emit-raw-abi-rust.d.ts.map +0 -1
  77. package/dist/generator/emit-raw-abi-rust.js +0 -380
  78. package/dist/generator/emit-raw-abi-rust.js.map +0 -1
  79. package/dist/generator/emit-rust.d.ts +0 -25
  80. package/dist/generator/emit-rust.d.ts.map +0 -1
  81. package/dist/generator/emit-rust.js +0 -125
  82. package/dist/generator/emit-rust.js.map +0 -1
  83. package/dist/generator/emit-straightline-go.d.ts +0 -60
  84. package/dist/generator/emit-straightline-go.d.ts.map +0 -1
  85. package/dist/generator/emit-straightline-go.js.map +0 -1
  86. package/dist/generator/emit-straightline-native-go.d.ts +0 -54
  87. package/dist/generator/emit-straightline-native-go.d.ts.map +0 -1
  88. package/dist/generator/emit-straightline-native-go.js +0 -835
  89. package/dist/generator/emit-straightline-native-go.js.map +0 -1
  90. package/dist/generator/emit-straightline-native-rust.d.ts +0 -27
  91. package/dist/generator/emit-straightline-native-rust.d.ts.map +0 -1
  92. package/dist/generator/emit-straightline-native-rust.js +0 -624
  93. package/dist/generator/emit-straightline-native-rust.js.map +0 -1
  94. package/dist/generator/emit-straightline-php.d.ts +0 -46
  95. package/dist/generator/emit-straightline-php.d.ts.map +0 -1
  96. package/dist/generator/emit-straightline-php.js +0 -941
  97. package/dist/generator/emit-straightline-php.js.map +0 -1
  98. package/dist/generator/emit-straightline-python.d.ts +0 -36
  99. package/dist/generator/emit-straightline-python.d.ts.map +0 -1
  100. package/dist/generator/emit-straightline-python.js +0 -855
  101. package/dist/generator/emit-straightline-python.js.map +0 -1
  102. package/dist/generator/emit-straightline-rust.d.ts.map +0 -1
  103. package/dist/generator/emit-straightline-rust.js.map +0 -1
  104. package/dist/generator/emit-straightline-typed-go.d.ts.map +0 -1
  105. package/dist/generator/emit-straightline-typed-go.js +0 -1114
  106. package/dist/generator/emit-straightline-typed-go.js.map +0 -1
  107. package/dist/generator/emit-straightline-typed-rust.d.ts.map +0 -1
  108. package/dist/generator/emit-straightline-typed-rust.js +0 -1155
  109. package/dist/generator/emit-straightline-typed-rust.js.map +0 -1
  110. package/dist/generator/emit-straightline-typescript.d.ts +0 -47
  111. package/dist/generator/emit-straightline-typescript.d.ts.map +0 -1
  112. package/dist/generator/emit-straightline-typescript.js.map +0 -1
@@ -1,8 +1,9 @@
1
1
  import { GeneratorFailure } from "./core.js";
2
- import { goStraightlineInternals } from "./emit-straightline-go.js";
3
- import { goTypedInternals } from "./emit-straightline-typed-go.js";
2
+ import { goStraightlineInternals } from "./emit-shared-go.js";
3
+ import { goTypedInternals } from "./emit-shared-go-typed.js";
4
4
  import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
5
5
  import { concurrencyPlan, execPlan, analyzeSequentialOps, classifyExpr, buildComponentPlan, } from "./straightline.js";
6
+ import { NativeExprCompiler } from "./native-expr.js";
6
7
  const PKG = "dslcontracts";
7
8
  /** goErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
8
9
  * plane (runtime-free). The covered module declares `type BehaviorError struct { Code, Message string }`
@@ -47,7 +48,7 @@ function portsStructName(compName, nodeId) {
47
48
  // type to implement the per-component Handler_<comp> interface's concrete Node_* method signatures.
48
49
  return `PortsNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
49
50
  }
50
- function inferPortType(node, inputPorts) {
51
+ function inferPortType(node, inputPorts, asBinding) {
51
52
  if (staticArrElems(node) !== null)
52
53
  return "Value";
53
54
  const e = classifyExpr(node);
@@ -58,6 +59,18 @@ function inferPortType(node, inputPorts) {
58
59
  case "bool":
59
60
  return "bool";
60
61
  case "ref": {
62
+ // #108: a `$as`-headed ref (map element binding) — resolve the element field's scalar type.
63
+ if (asBinding !== undefined && e.path[0] === asBinding.name) {
64
+ try {
65
+ const { ref } = goTypedInternals.typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan);
66
+ if (ref.kind === "scalar" && ref.scalar !== "null")
67
+ return goScalarPortField(ref.scalar);
68
+ }
69
+ catch {
70
+ return "Value";
71
+ }
72
+ return "Value";
73
+ }
61
74
  if (e.path.length === 1) {
62
75
  const s = inputPorts[e.path[0]];
63
76
  if (s) {
@@ -77,6 +90,10 @@ function inferPortType(node, inputPorts) {
77
90
  return "Value";
78
91
  }
79
92
  }
93
+ /** scalar TypeRef → PortFieldType (native go scalar kind). */
94
+ function goScalarPortField(scalar) {
95
+ return scalar === "string" ? "string" : scalar === "int" ? "int64" : scalar === "float" ? "float64" : "bool";
96
+ }
80
97
  /** A static string-array port (every element a static string literal) — the graphddb `projection`
81
98
  * shape. Returns the list of literal strings, or null when the node is not that shape. */
82
99
  function staticStringArrElems(node) {
@@ -102,13 +119,17 @@ function staticStringArrElems(node) {
102
119
  * Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no
103
120
  * `dslcontracts.Value` fallback on the native module). `where` names the emission site for the error.
104
121
  */
105
- function portFieldGoType(node, inputPorts, where = "port") {
122
+ function portFieldGoType(node, inputPorts, where = "port", asBinding, plan) {
106
123
  if (staticStringArrElems(node) !== null)
107
124
  return "[]string";
125
+ // #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → []ElemT.
126
+ const arrElem = portArrayElemRef(node, inputPorts, plan);
127
+ if (arrElem !== null)
128
+ return `[]${renderTypeRef(arrElem)}`;
108
129
  const num = numLiteralGoExpr(node);
109
130
  if (num !== null)
110
131
  return Number.isInteger(node) ? "int64" : "float64";
111
- const ft = inferPortType(node, inputPorts);
132
+ const ft = inferPortType(node, inputPorts, asBinding);
112
133
  if (ft === "Value") {
113
134
  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.`);
114
135
  }
@@ -123,6 +144,36 @@ function staticArrElems(node) {
123
144
  const arg = node.arr;
124
145
  return Array.isArray(arg) ? arg : null;
125
146
  }
147
+ /**
148
+ * portArrayElemRef (#110) — resolve a componentRef input port that binds a RUNTIME ARRAY (an IN-list /
149
+ * array-bound WHERE head) to its native element TypeRef, or null when the port is not a covered array.
150
+ * The covered shape is a SINGLE-SEGMENT, non-opt `ref` into an INPUT ARRAY port that carries a resolvable
151
+ * `elemType` (scp-ir-architecture.md §5.2 / PortSchema.elemType — BC does NOT infer, consumer-interface
152
+ * C3). The port then lowers to a native `[]ElemT` field fed straight from `in.<Field>` — ZERO boxed Value
153
+ * on the read hot path. This REUSES the bc#108 `elemType` element lowering (deriveTypeRef over the input
154
+ * port's elemType), extended from the `map…over` element-source path to a plain componentRef port.
155
+ *
156
+ * A static string array is a DIFFERENT covered shape ([]string, staticStringArrElems) — handled ahead of
157
+ * this. An array port with NO resolvable elemType, an opt/multi-segment ref, or a non-array input schema
158
+ * resolves to null here → the caller keeps its existing fail-closed behaviour (never a silent box).
159
+ * Requires a TypePlan (an obj elemType maps to a named decl); without one it stays null (fail-closed).
160
+ */
161
+ function portArrayElemRef(node, inputPorts, plan) {
162
+ if (plan === undefined)
163
+ return null;
164
+ if (staticStringArrElems(node) !== null)
165
+ return null; // the static-string-array port ([]string) — not this shape.
166
+ const e = classifyExpr(node);
167
+ if (e.kind !== "ref" || e.opt || e.path.length !== 1)
168
+ return null;
169
+ const schema = inputPorts?.[e.path[0]];
170
+ if (schema === undefined || (schema.type !== "array" && schema.type !== "arr"))
171
+ return null;
172
+ const et = schema.elemType;
173
+ if (et === undefined)
174
+ return null;
175
+ return deriveTypeRef(et, plan);
176
+ }
126
177
  /**
127
178
  * isNativeRawComponent — a component covered by the combined read emitter. Strictly sequential
128
179
  * (no real concurrency), body is ALL componentRef (no map/cond), default 'fail'/'continue'
@@ -236,27 +287,35 @@ function isNativeRaw(comp) {
236
287
  * element struct by copying the over element's typed fields + materializing the into field from the
237
288
  * aligned batch raw — NO boxed Value on any plane.
238
289
  */
239
- function coveredMapNode(n, bodyIds) {
290
+ function coveredMapNode(n, bodyIds, comp) {
240
291
  if (n === null || typeof n !== "object" || !("map" in n))
241
292
  return false;
242
293
  const m = n.map;
243
- // #86 pt2: batched map...into. #93 shape 2: NON-batched map...into (per-element dispatch). #93
244
- // shape 3: batched map with NO into (connection-collect / plain list). A `when` guard + into is
245
- // still uncovered (heterogeneous elements not one []T). A `when` guard WITHOUT into would be
246
- // covered in principle but the covered corpus does not exercise it; keep it out to stay conservative.
247
- if (m.when !== undefined)
294
+ // #86 pt2: batched map...into. #93 shape 2/3: (non-)batched map...into / connection-collect. #108:
295
+ // a guarded map (`when`) IS covered when it has NO `into` (guard+into is heterogeneous — skipped
296
+ // elements lack the into field, cannot de-box into one []T; keep it fail-closed, matching the typed
297
+ // straightline reject). A guarded map over a native-typed element collection drops skipped elements.
298
+ if (m.when !== undefined && m.into !== undefined)
248
299
  return false;
249
300
  if (m.policy !== undefined && m.policy !== "fail")
250
301
  return false;
251
302
  if (m.relationKind !== undefined && m.relationKind !== "single" && m.relationKind !== "connection")
252
303
  return false;
253
- // `over` must be a ref to a PRIOR BODY NODE's typed arr field (the de-box resolves the over element
254
- // type from the parent node's typed struct mapOverElemType). A map over an INPUT-param array is
255
- // not covered here (its element type comes from inputPorts, not a typed node struct) — it falls
256
- // through to the boxed typed / straight-line path.
304
+ // `over` element type must be STATICALLY resolvable: a ref to a PRIOR BODY NODE's typed arr field
305
+ // (mapOverElemType) OR a ref to an INPUT array port that carries `elemType` (#108 the C3 "BC does
306
+ // not infer" rule: an input array WITHOUT elemType stays fail-closed / uncovered).
257
307
  const overE = classifyExpr(m.over);
258
- if (overE.kind !== "ref" || !bodyIds.has(overE.path[0]))
308
+ if (overE.kind !== "ref")
259
309
  return false;
310
+ const head = overE.path[0];
311
+ if (!bodyIds.has(head)) {
312
+ // input-array over: covered only when the input port declares elemType (#108).
313
+ if (overE.path.length !== 1)
314
+ return false;
315
+ const schema = comp.inputPorts?.[head];
316
+ if (schema === undefined || schema.elemType === undefined)
317
+ return false;
318
+ }
260
319
  const ports = m.ports;
261
320
  for (const p of Object.keys(ports))
262
321
  if (!portIsStatic(ports[p]))
@@ -295,13 +354,21 @@ function coverageRejectReason(comp) {
295
354
  }
296
355
  const bodyIds = new Set(comp.body.map((b) => b.id));
297
356
  for (const n of comp.body) {
298
- if ("cond" in n)
299
- return `node '${n.id}' is a cond branch (not yet native-covered)`;
357
+ if ("cond" in n) {
358
+ // #108: a cond node is native-covered its `if`/`then`/`else` compile to native expressions via
359
+ // the runtime-free NativeExprCompiler (byte-equal to run_behavior's cond lower). If an expression
360
+ // is not statically compilable (uncovered op / refOpt / opt operand / type mismatch), the compiler
361
+ // fails closed at emit (a loud GeneratorFailure) — never a silent box. cond requires outType.
362
+ if (n.outType === undefined)
363
+ return `node '${n.id}' cond has no outType (required to de-box the branch join)`;
364
+ continue;
365
+ }
300
366
  if ("map" in n) {
301
- // #86 pt2 batched map...into / #93 shape 2 non-batched map...into / #93 shape 3 batched map NO
302
- // into ARE covered (over a prior body node's typed arr); other map shapes fall through.
303
- if (!coveredMapNode(n, bodyIds))
304
- return `node '${n.id}' is a non-covered map shape (guarded/over-input)`;
367
+ // #86 pt2 batched map...into / #93 shape 2/3 / #108 guarded map (no into) / map-over-input (with
368
+ // elemType) ARE covered. A non-covered map shape (guard+into heterogeneous / over-input without
369
+ // elemType) falls through to the loud fail-closed reject.
370
+ if (!coveredMapNode(n, bodyIds, comp))
371
+ return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
305
372
  continue;
306
373
  }
307
374
  if (!("component" in n))
@@ -342,13 +409,13 @@ function assertTypedCoverage(comp) {
342
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).`);
343
410
  }
344
411
  // ── native ports struct + PortReader ──────────────────────────────────────────────────
345
- function emitPortsStruct(comp, node) {
412
+ function emitPortsStruct(comp, node, asBinding, plan) {
346
413
  const structName = portsStructName(comp.name, node.id);
347
414
  const portNames = Object.keys(node.ports);
348
415
  if (portNames.length === 0) {
349
416
  return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
350
417
  }
351
- const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp.inputPorts));
418
+ const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding, plan));
352
419
  const fieldNames = portNames.map(goPortFieldName);
353
420
  const fields = portNames
354
421
  .map((p, i) => `\t${fieldNames[i]} ${fieldTypes[i]} // ${JSON.stringify(p)}`)
@@ -391,6 +458,14 @@ function rawElemStructName(compName, nodeId) {
391
458
  function handlerIfaceName(compName) {
392
459
  return `Handler_${sanitize(compName)}`;
393
460
  }
461
+ /** RunNativeRawStruct_<comp> — the EXPORTED name of the per-component struct-returning combined runner
462
+ * (bc#102). It MUST be an exported Go identifier so a consumer in a DIFFERENT package can call it (a
463
+ * leading-lowercase name is unexported / uncallable cross-package). The `RunNativeRawStruct_` prefix
464
+ * guarantees the leading char is uppercase regardless of how the component name sanitizes, and the
465
+ * sanitized component suffix keeps it unique per covered component. */
466
+ function runnerName(compName) {
467
+ return `RunNativeRawStruct_${sanitize(compName)}`;
468
+ }
394
469
  /** Node_<comp>_<nodeId> — the concrete per-node handler method name. */
395
470
  function nodeMethodName(compName, nodeId) {
396
471
  return `Node_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
@@ -404,6 +479,9 @@ function nodeMethodName(compName, nodeId) {
404
479
  function emitRawRowStructs(comp, typedNodes, plan) {
405
480
  const parts = [];
406
481
  for (const n of comp.body) {
482
+ // #108: a cond node has NO handler-result row (pure Expression — no dispatch).
483
+ if ("cond" in n)
484
+ continue;
407
485
  const ref = typedNodes.get(n.id);
408
486
  if ("map" in n) {
409
487
  // a covered map node: RawElem_<comp>_<node> is the HANDLER'S per-element return shape:
@@ -498,6 +576,8 @@ function emitHandlerInterface(comp, typedNodes, plan) {
498
576
  lines.push(`// Node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
499
577
  lines.push(`type ${handlerIfaceName(comp.name)} interface {`);
500
578
  for (const n of comp.body) {
579
+ if ("cond" in n)
580
+ continue; // #108: a cond node has no handler method (pure Expression).
501
581
  const method = nodeMethodName(comp.name, n.id);
502
582
  const bt = boundGoType(n, comp, typedNodes, plan);
503
583
  if ("map" in n) {
@@ -524,10 +604,15 @@ function mapElemMaterializerName(compName, nodeId) {
524
604
  * slice of the concrete per-element ports structs (NO `[]dslcontracts.Value`, NO PortReader accessor);
525
605
  * the consumer reads `bp.Items[i].F_PK` directly.
526
606
  */
527
- function emitMapPortsStructs(comp, node) {
607
+ function emitMapPortsStructs(comp, node, typedNodes, plan) {
528
608
  const m = node.map;
529
609
  const structName = portsStructName(comp.name, node.id);
530
- const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports });
610
+ // #108: the element ports may read the `$as` binding ($id / $g.field) resolve the over element
611
+ // type so those ports type their struct field natively (map-over-input scalar element / prior-node
612
+ // struct element). A pure-static/concat port ignores the binding.
613
+ const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
614
+ const asBinding = { name: m.as, ref: elemRef, plan };
615
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan);
531
616
  const batchStruct = `${structName}_batch`;
532
617
  return `${elemStruct}
533
618
 
@@ -596,19 +681,35 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
596
681
  }
597
682
  return parts.join("\n\n");
598
683
  }
599
- /** The Go type of the over-element (the parent collection element) for a covered map node. `over`
600
- * is a static ref to a prior node's arr<named> field (or an input array). We resolve it via the
601
- * parent node's typed struct field type. */
602
- function mapOverElemType(comp, node, typedNodes, plan) {
684
+ /**
685
+ * mapOverElemInfo resolve a covered map's `over` collection: its element TypeRef + the Go expression
686
+ * that yields the native over slice. Two sources (#108):
687
+ * - a ref to a PRIOR BODY NODE's typed arr field (`t_root.Groups`) — element type from the parent struct;
688
+ * - a ref to an INPUT ARRAY port carrying `elemType` (`in.Ids`) — element type from PortSchema.elemType.
689
+ * A map over an input array WITHOUT elemType is fail-closed (BC does not infer — consumer-interface C3).
690
+ */
691
+ function mapOverElemInfo(comp, node, typedNodes, plan) {
603
692
  const m = node.map;
604
693
  const e = classifyExpr(m.over);
605
694
  if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
606
695
  const baseRef = typedNodes.get(e.path[0]);
607
- const { ref } = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
696
+ const { expr, ref } = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
608
697
  if (ref.kind === "arr")
609
- return renderTypeRef(ref.elem);
698
+ return { elemRef: ref.elem, overExpr: expr };
610
699
  }
611
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined map (#86 pt2): map '${node.id}' 'over' must be a ref to a prior node's typed arr field (got ${JSON.stringify(m.over)}).`);
700
+ // input-array over (#108): the head is an input port; its declared elemType is the element type.
701
+ if (e.kind === "ref" && e.path.length === 1) {
702
+ const schema = comp.inputPorts?.[e.path[0]];
703
+ const et = schema?.elemType;
704
+ if (et !== undefined) {
705
+ return { elemRef: deriveTypeRef(et, plan), overExpr: `in.${goFieldName(e.path[0])}` };
706
+ }
707
+ }
708
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined map (#108): map '${node.id}' 'over' must be a ref to a prior node's typed arr field OR an input array port with a declared elemType (got ${JSON.stringify(m.over)}). BC does not infer input-array element types (consumer-interface C3).`);
709
+ }
710
+ /** The Go type of the over-element (back-compat shim). */
711
+ function mapOverElemType(comp, node, typedNodes, plan) {
712
+ return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
612
713
  }
613
714
  function classifyPort(node) {
614
715
  if (staticArrElems(node) !== null)
@@ -663,8 +764,8 @@ function reconstructG(e) {
663
764
  function inStructName(compName) {
664
765
  return `In_${sanitize(compName)}`;
665
766
  }
666
- /** Go type for an input port's declared type (scalar → concrete; anything else Value). */
667
- function inputPortGoType(schema) {
767
+ /** Go type for an input port's declared type (scalar → concrete; array+elemType → []ElemT; else Value). */
768
+ function inputPortGoType(schema, plan) {
668
769
  switch (schema?.type) {
669
770
  case "string":
670
771
  return "string";
@@ -674,6 +775,15 @@ function inputPortGoType(schema) {
674
775
  return "float64";
675
776
  case "bool":
676
777
  return "bool";
778
+ case "array":
779
+ case "arr": {
780
+ // #108: an input ARRAY port with a declared elemType lowers to a native []ElemT (the map-over-input
781
+ // $as element type source). Without elemType or a plan, it stays a boxed Value (uncovered / fail-closed).
782
+ const et = schema.elemType;
783
+ if (et !== undefined && plan !== undefined)
784
+ return `[]${renderTypeRef(deriveTypeRef(et, plan))}`;
785
+ return `${PKG}.Value`;
786
+ }
677
787
  default:
678
788
  return `${PKG}.Value`;
679
789
  }
@@ -695,14 +805,14 @@ function inputScalarKind(schema) {
695
805
  }
696
806
  /** emitInStruct — the concrete input struct declaration for a covered component (fields = inputPorts,
697
807
  * declaration order = Object key order; gofmt-aligned). Empty inputPorts => empty struct. */
698
- function emitInStruct(comp) {
808
+ function emitInStruct(comp, plan) {
699
809
  const name = inStructName(comp.name);
700
810
  const ports = Object.keys(comp.inputPorts ?? {});
701
811
  if (ports.length === 0) {
702
812
  return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
703
813
  }
704
814
  const names = ports.map((p) => goFieldName(p));
705
- const types = ports.map((p) => inputPortGoType(comp.inputPorts[p]));
815
+ const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan));
706
816
  const nameW = Math.max(0, ...names.map((n) => n.length));
707
817
  const typeW = Math.max(0, ...types.map((t) => t.length));
708
818
  const fields = ports
@@ -722,7 +832,7 @@ ${fields}
722
832
  * pre-built by the consumer. A missing key leaves the zero value (every covered input is required +
723
833
  * always supplied by the corpus); a present-but-wrong-scalar is a fail-closed decode error.
724
834
  */
725
- function emitInDecoder(comp) {
835
+ function emitInDecoder(comp, plan) {
726
836
  const name = inStructName(comp.name);
727
837
  const ports = Object.keys(comp.inputPorts ?? {});
728
838
  const lines = [];
@@ -739,8 +849,28 @@ function emitInDecoder(comp) {
739
849
  lines.push(`\t}`);
740
850
  for (const p of ports) {
741
851
  const field = goFieldName(p);
742
- const scalar = inputScalarKind(comp.inputPorts[p]);
852
+ const schema = comp.inputPorts[p];
853
+ const scalar = inputScalarKind(schema);
854
+ const et = schema.elemType;
743
855
  lines.push(`\tif v, ok := input.Get(${JSON.stringify(p)}); ok {`);
856
+ if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
857
+ // #108: decode the boxed Value array into the native []ElemT (test glue — materialize each element
858
+ // via the typed materializer; a non-array or non-conforming element is a fail-closed decode error).
859
+ const elemRef = deriveTypeRef(et, plan);
860
+ const elemGo = renderTypeRef(elemRef);
861
+ lines.push(`\t\tarr, isArr := v.([]${PKG}.Value)`);
862
+ lines.push(`\t\tif !isArr {`);
863
+ lines.push(`\t\t\treturn in, ${PKG}.NewExprFailure("TYPE_MISMATCH", "input ${p}: expected array, got "+${PKG}.TypeName(v))`);
864
+ lines.push(`\t\t}`);
865
+ lines.push(`\t\tdec := make([]${elemGo}, 0, len(arr))`);
866
+ lines.push(`\t\tfor _, ev := range arr {`);
867
+ // materializeExpr returns the must*-based decode (panics on a malformed corpus element — test glue).
868
+ lines.push(`\t\t\tdec = append(dec, ${goTypedInternals.materializeExpr("ev", elemRef, plan)})`);
869
+ lines.push(`\t\t}`);
870
+ lines.push(`\t\tin.${field} = dec`);
871
+ lines.push(`\t}`);
872
+ continue;
873
+ }
744
874
  if (scalar === undefined) {
745
875
  lines.push(`\t\tin.${field} = v`);
746
876
  }
@@ -812,8 +942,18 @@ function emitNativeScalar(node, want, resolveRef) {
812
942
  * plane — a boxed escape would re-introduce the bc-runtime coupling this module removes). Returns the
813
943
  * struct field initializer (`Field: <expr>`). `resolveRef` resolves a ref head for the scalar path.
814
944
  */
815
- function emitPortInit(pn, expr, fieldGoType, resolveRef) {
945
+ function emitPortInit(pn, expr, fieldGoType, resolveRef, inputPorts, plan) {
816
946
  const fieldName = goPortFieldName(pn);
947
+ // #110: a componentRef port bound to a runtime array input port (with elemType) → the native []ElemT,
948
+ // read straight from the concrete input struct (`in.<Field>`) — ZERO boxed Value on the read hot path.
949
+ // The over-elem/element-source case (map…over) already resolves via mapOverElemInfo; this is the plain
950
+ // componentRef port (an IN-list / array-bound WHERE head).
951
+ if (inputPorts !== undefined && portArrayElemRef(expr, inputPorts, plan) !== null) {
952
+ const e = classifyExpr(expr);
953
+ if (e.kind === "ref" && e.path.length === 1) {
954
+ return `${fieldName}: in.${goFieldName(e.path[0])}`;
955
+ }
956
+ }
817
957
  // change #2: a static string array (projection) → a native `[]string{...}` literal.
818
958
  const strArr = staticStringArrElems(expr);
819
959
  if (strArr !== null) {
@@ -899,8 +1039,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
899
1039
  const resolveRef = resolveRefAt(atPos);
900
1040
  for (const pn of Object.keys(node.ports)) {
901
1041
  const expr = node.ports[pn];
902
- const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`);
903
- inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef));
1042
+ const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan);
1043
+ inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef, comp.inputPorts, plan));
904
1044
  }
905
1045
  lines.push(`${ind}ps_${s} = ${structName}{${inits.join(", ")}}`);
906
1046
  lines.push(`${ind}run_${s} = true`);
@@ -985,7 +1125,7 @@ function emitNativeRawRunner(comp, plan, flags) {
985
1125
  const outStructType = outRef ? renderTypeRef(outRef) : `${PKG}.Value`;
986
1126
  const zeroOut = outRef ? goZero(outRef) : "nil";
987
1127
  const lines = [];
988
- lines.push(`// run_native_raw_struct_${fn} — the STRUCT-RETURNING combined read (bc#77/#87): the fully`);
1128
+ lines.push(`// ${runnerName(comp.name)} — the STRUCT-RETURNING combined read (bc#77/#87): the fully`);
989
1129
  lines.push(`// de-plumbed path. Ports are a native struct (direct field assignment); the handler`);
990
1130
  lines.push(`// result is materialized straight into the node's outType struct. Node results are typed`);
991
1131
  lines.push(`// struct locals; a relation child reads the parent's REAL struct result via direct field`);
@@ -1001,7 +1141,7 @@ function emitNativeRawRunner(comp, plan, flags) {
1001
1141
  // whose Node_* methods return the concrete per-node row struct (typed fields = the node's outType).
1002
1142
  // The covered result plane carries no boxed ports / boxed value / dynamic accessor / semantic type-
1003
1143
  // assert on a row value — the runner reads row.<Field> directly and copies each into the outType local.
1004
- lines.push(`func run_native_raw_struct_${fn}[H ${handlerIfaceName(comp.name)}](handlers H, in ${inStructName(comp.name)}) (${outStructType}, error) {`);
1144
+ lines.push(`func ${runnerName(comp.name)}[H ${handlerIfaceName(comp.name)}](handlers H, in ${inStructName(comp.name)}) (${outStructType}, error) {`);
1005
1145
  lines.push(`\t_ = in`);
1006
1146
  // resolveNativeRef for this runner: a ref head is either a PRIOR NODE (read its typed struct field
1007
1147
  // directly) or an INPUT PORT (read the typed `in.<Field>`). A REQUIRED scalar resolves to a native
@@ -1057,6 +1197,13 @@ function emitNativeRawRunner(comp, plan, flags) {
1057
1197
  lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut, flags));
1058
1198
  continue;
1059
1199
  }
1200
+ // #108: a cond node — pure Expression (no handler). Compile `if` to a native bool + materialize the
1201
+ // taken branch to the node's outType, assigning the node's typed cell. Byte-equal to run_behavior's
1202
+ // cond lower ({cond:[if,then,else]}, strict-bool, only the taken branch evaluated).
1203
+ if ("cond" in comp.body[i]) {
1204
+ lines.push(emitCondArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut));
1205
+ continue;
1206
+ }
1060
1207
  const node = comp.body[i];
1061
1208
  const meta = metas[i];
1062
1209
  const s = sanitize(node.id);
@@ -1101,8 +1248,8 @@ function emitNativeRawRunner(comp, plan, flags) {
1101
1248
  const resolveRef = resolveRefAt(k);
1102
1249
  for (const pn of portNames) {
1103
1250
  const expr = node.ports[pn];
1104
- const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`);
1105
- inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef));
1251
+ const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan);
1252
+ inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef, comp.inputPorts, plan));
1106
1253
  }
1107
1254
  lines.push(`${indent}ports_${s} := ${structName}{${inits.join(", ")}}`);
1108
1255
  const oc = `row_${s}`;
@@ -1174,22 +1321,37 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1174
1321
  const m = node.map;
1175
1322
  const batched = m.batched === true;
1176
1323
  const hasInto = m.into !== undefined;
1324
+ const hasGuard = m.when !== undefined;
1177
1325
  const s = sanitize(node.id);
1178
1326
  const structName = portsStructName(comp.name, node.id);
1179
1327
  const batchStruct = `${structName}_batch`;
1180
1328
  const arrRef = typedNodes.get(node.id);
1181
1329
  const elemGo = renderTypeRef(arrRef);
1182
1330
  const asName = m.as;
1183
- const overElemGo = mapOverElemType(comp, node, typedNodes, plan);
1184
- // over element type ref (for $as field access): resolve the parent field's arr element type.
1185
- const overE = classifyExpr(m.over);
1186
- const overParent = overE.kind === "ref" ? overE.path[0] : "";
1187
- const overBaseRef = typedNodes.get(overParent);
1188
- const { expr: overExpr, ref: overRef } = goTypedInternals.typedFieldAccess(typedLocal(overParent), overBaseRef, overE.kind === "ref" ? overE.path.slice(1) : [], plan);
1189
- const overElemRef = overRef.elem;
1331
+ // over collection element type + the native over-slice expression (#108: prior-node arr OR input array
1332
+ // with elemType). The $as element is a value of `overElemRef` (input-array element or prior-node elem).
1333
+ const { elemRef: overElemRef, overExpr } = mapOverElemInfo(comp, node, typedNodes, plan);
1334
+ const overElemGo = renderTypeRef(overElemRef);
1190
1335
  const elemMat = mapElemMaterializerName(comp.name, node.id);
1191
1336
  const priorHere = (head) => priorNodeAt(head, atPos);
1192
1337
  const portNames = Object.keys(m.ports);
1338
+ // #108 guard: compile the `when` expression to a native bool over an element-scoped resolveHead
1339
+ // ($as → the over element value `elemVar`; prior node → its typed local; input port → in.<Field>).
1340
+ // Returns the compiled {stmts, expr} or null (no guard). Mirrors run_behavior's per-element keep-gate
1341
+ // ({cond:[when,true,false]}, strict-bool). A non-bool / uncovered guard expr fails closed at emit.
1342
+ const mkGuard = (elemVar) => {
1343
+ if (!hasGuard)
1344
+ return null;
1345
+ const resolveHead = (head) => {
1346
+ if (head === asName)
1347
+ return { expr: elemVar, ref: overElemRef };
1348
+ if (priorHere(head))
1349
+ return { expr: typedLocal(head), ref: typedNodes.get(head) };
1350
+ return goInputHeadRef(head, comp, plan);
1351
+ };
1352
+ const be = makeGoExprBackend(zeroOut, resolveHead, plan);
1353
+ return new NativeExprCompiler(be).compileBool(m.when);
1354
+ };
1193
1355
  // emit the per-element native ports struct build (shared; `il` is the indent inside the element
1194
1356
  // loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
1195
1357
  const buildPorts = (il, elemVar) => {
@@ -1233,9 +1395,10 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1233
1395
  return null;
1234
1396
  return { expr, scalar: goScalar(acc.scalar) };
1235
1397
  };
1398
+ const asBinding = { name: asName, ref: overElemRef, plan };
1236
1399
  for (const pn of portNames) {
1237
- const fieldGoType = portFieldGoType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`);
1238
- inits.push(emitPortInit(pn, m.ports[pn], fieldGoType, resolveRef));
1400
+ const fieldGoType = portFieldGoType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding, plan);
1401
+ inits.push(emitPortInit(pn, m.ports[pn], fieldGoType, resolveRef, comp.inputPorts, plan));
1239
1402
  }
1240
1403
  out.push(`${il}ep_${s} := ${structName}{${inits.join(", ")}}`);
1241
1404
  return out;
@@ -1254,41 +1417,69 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1254
1417
  lines.push(`${indent}over_${s} := ${overExpr}`);
1255
1418
  lines.push(`${indent}${typedLocal(node.id)} = make(${elemGo}, 0, len(over_${s}))`);
1256
1419
  const elemRowRef = mapElemRowRef(node, arrRef, plan);
1420
+ // emit the per-element guard keep-gate (#108). `il` = indent, `elemVar` = the over element loop var.
1421
+ // On skip: batched → `continue` (element excluded from items + keptOver); non-batched → `continue`.
1422
+ const emitGuard = (il, elemVar) => {
1423
+ const g = mkGuard(elemVar);
1424
+ if (g === null)
1425
+ return [];
1426
+ const out = [];
1427
+ for (const st of g.stmts)
1428
+ out.push(`${il}${st}`);
1429
+ out.push(`${il}if !(${g.expr}) {`);
1430
+ out.push(`${il}\tcontinue`);
1431
+ out.push(`${il}}`);
1432
+ return out;
1433
+ };
1257
1434
  if (batched) {
1258
- // BATCHED (#86 pt2 into / #93 shape 3 no-into): build all ports, dispatch ONCE. The concrete
1259
- // Node_* returns RawRow_<comp>_<node>{IsError, Err, Rows []RawElem_} aligned to the over items.
1435
+ // BATCHED (#86 pt2 into / #93 shape 3 no-into / #108 guarded no-into): build kept-element ports,
1436
+ // dispatch ONCE. The concrete Node_* returns RawRow{IsError,Err,Rows []RawElem} aligned to items.
1437
+ // With a guard, `items` (and, for into, `keptOver`) hold only kept elements — matching run_behavior's
1438
+ // keptIdx-aligned batch. `into` is never combined with a guard (rejected). no-into collects each row.
1260
1439
  lines.push(`${indent}items_${s} := make([]${structName}, 0, len(over_${s}))`);
1440
+ if (hasInto)
1441
+ lines.push(`${indent}keptOver_${s} := make([]${overElemGo}, 0, len(over_${s}))`);
1261
1442
  lines.push(`${indent}for _, ${elemLocal(s)} := range over_${s} {`);
1443
+ for (const l of emitGuard(indent + "\t", elemLocal(s)))
1444
+ lines.push(l);
1262
1445
  for (const l of buildPorts(indent + "\t", elemLocal(s)))
1263
1446
  lines.push(l);
1264
1447
  lines.push(`${indent}\titems_${s} = append(items_${s}, ep_${s})`);
1448
+ if (hasInto)
1449
+ lines.push(`${indent}\tkeptOver_${s} = append(keptOver_${s}, ${elemLocal(s)})`);
1265
1450
  lines.push(`${indent}}`);
1266
- // empty over => empty result (handler not called), matching run_behavior.
1451
+ // empty items => empty result (handler not called), matching run_behavior.
1267
1452
  lines.push(`${indent}if len(items_${s}) > 0 {`);
1268
1453
  lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: items_${s}}`);
1269
1454
  lines.push(`${indent}\tmo_${s}, mo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
1270
1455
  lines.push(`${indent}\tif !mo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${m.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
1271
1456
  lines.push(`${indent}\tif mo_${s}.IsError {`, `${indent}\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under 'fail' policy: "+mo_${s}.Err`)}`, `${indent}\t}`);
1272
1457
  lines.push(`${indent}\tif len(mo_${s}.Rows) != len(items_${s}) {`, `${indent}\t\treturn ${zeroOut}, ${goErr("MAP_BATCH_RESULT_MISMATCH", `fmt.Sprintf("map '${node.id}': batched handler must return a list aligned to items (want %d)", len(items_${s}))`)}`, `${indent}\t}`);
1273
- lines.push(`${indent}\tfor mk_${s} := range over_${s} {`);
1274
1458
  if (hasInto) {
1275
- // #86 pt2: zip-attach the into field onto each over element (augmented element copier).
1276
- lines.push(`${indent}\t\tel_${s} := ${elemMat}(over_${s}[mk_${s}], mo_${s}.Rows[mk_${s}])`);
1459
+ // #86 pt2: zip-attach the into field onto each kept over element (augmented element copier).
1460
+ lines.push(`${indent}\tfor mk_${s} := range keptOver_${s} {`);
1461
+ lines.push(`${indent}\t\tel_${s} := ${elemMat}(keptOver_${s}[mk_${s}], mo_${s}.Rows[mk_${s}])`);
1462
+ lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1463
+ lines.push(`${indent}\t}`);
1277
1464
  }
1278
1465
  else {
1279
- // #93 shape 3: collect each element DIRECTLY from the concrete per-element row (field copy).
1466
+ // #93 shape 3 / #108: collect each element DIRECTLY from the aligned per-element row (field copy).
1467
+ lines.push(`${indent}\tfor mk_${s} := range mo_${s}.Rows {`);
1280
1468
  lines.push(`${indent}\t\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
1281
1469
  lines.push(emitRowCopy(`el_${s}`, `mo_${s}.Rows[mk_${s}]`, elemRowRef, plan, indent.length + 2));
1470
+ lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1471
+ lines.push(`${indent}\t}`);
1282
1472
  }
1283
- lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1284
- lines.push(`${indent}\t}`);
1285
1473
  lines.push(`${indent}}`);
1286
1474
  }
1287
1475
  else {
1288
- // #93 shape 2: NON-batched map...into — dispatch the child handler ONCE PER ELEMENT (N physical
1289
- // requests, matching run_behavior's per-element loop). The concrete Node_* returns ONE RawElem_.
1476
+ // #93 shape 2 / #108: NON-batched map — dispatch the child handler ONCE PER KEPT ELEMENT (N physical
1477
+ // requests, matching run_behavior's per-element loop). A guarded element that fails `when` is skipped
1478
+ // (no dispatch, excluded from the result). The concrete Node_* returns ONE RawElem_.
1290
1479
  lines.push(`${indent}for mk_${s} := range over_${s} {`);
1291
1480
  lines.push(`${indent}\t${elemLocal(s)} := over_${s}[mk_${s}]`);
1481
+ for (const l of emitGuard(indent + "\t", elemLocal(s)))
1482
+ lines.push(l);
1292
1483
  for (const l of buildPorts(indent + "\t", elemLocal(s)))
1293
1484
  lines.push(l);
1294
1485
  // per-element dispatch (ONE physical request per element). run_behavior passes the over element
@@ -1317,6 +1508,76 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1317
1508
  function elemLocal(s) {
1318
1509
  return `oel_${s}`;
1319
1510
  }
1511
+ /** #108: resolve an INPUT port head for a native-expr ref (cond `if` / map `when` / branches). A scalar
1512
+ * port → `in.<Field>` of the scalar type; an array port WITH elemType → `in.<Field>` of `[]ElemT` (so
1513
+ * `len(in.<Field>)` works); anything else → null (fail-closed). */
1514
+ function goInputHeadRef(head, comp, plan) {
1515
+ const schema = comp.inputPorts?.[head];
1516
+ if (schema === undefined)
1517
+ return null;
1518
+ const sc = inputScalarKind(schema);
1519
+ if (sc !== undefined) {
1520
+ const scalarRef = { kind: "scalar", scalar: sc === "int64" ? "int" : sc === "float64" ? "float" : sc };
1521
+ return { expr: `in.${goFieldName(head)}`, ref: scalarRef };
1522
+ }
1523
+ const et = schema.elemType;
1524
+ if ((schema.type === "array" || schema.type === "arr") && et !== undefined) {
1525
+ return { expr: `in.${goFieldName(head)}`, ref: { kind: "arr", elem: deriveTypeRef(et, plan) } };
1526
+ }
1527
+ return null;
1528
+ }
1529
+ /**
1530
+ * emitCondArm — the struct-native exec of a covered cond node (#108). Mirrors run_behavior's cond
1531
+ * lower (behavior.ts:425-429): compile `if` to a native bool, materialize the TAKEN branch to the
1532
+ * node's outType (only the taken branch evaluated — short-circuit), assign the node's typed cell and
1533
+ * set produced. NO handler dispatch (cond is a pure Expression). The `if`/`then`/`else` expressions
1534
+ * compile via the runtime-free NativeExprCompiler (byte-equal to `evaluate`); an uncovered op / opt /
1535
+ * refOpt / type-mismatch fails closed at emit.
1536
+ */
1537
+ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut) {
1538
+ const c = node.cond;
1539
+ const s = sanitize(node.id);
1540
+ const nodeRef = typedNodes.get(node.id);
1541
+ const priorHere = (head) => priorNodeAt(head, atPos);
1542
+ const resolveHead = (head) => {
1543
+ if (priorHere(head))
1544
+ return { expr: typedLocal(head), ref: typedNodes.get(head) };
1545
+ return goInputHeadRef(head, comp, plan);
1546
+ };
1547
+ const be = makeGoExprBackend(zeroOut, resolveHead, plan);
1548
+ const compiler = new NativeExprCompiler(be);
1549
+ const resolveDecl = (name) => findDecl(plan, name).fields;
1550
+ const cond = compiler.compileBool(c.if);
1551
+ const thenC = compiler.compileTo(c.then, nodeRef, resolveDecl);
1552
+ const elseC = compiler.compileTo(c.else, nodeRef, resolveDecl);
1553
+ const lines = [];
1554
+ lines.push(`\t// ── cond '${node.id}'${c.parent ? " (parent:" + c.parent + ")" : ""} — pure Expression (no handler) ──`);
1555
+ let indent = "\t";
1556
+ const closers = [];
1557
+ if (c.parent !== undefined && priorNodeAt(c.parent, atPos)) {
1558
+ lines.push(`${indent}if produced_${sanitize(c.parent)} {`);
1559
+ closers.unshift(`${indent}}`);
1560
+ indent += "\t";
1561
+ }
1562
+ // the `if` condition (its fallible temps hoisted before the branch — the condition is always evaluated).
1563
+ for (const st of cond.stmts)
1564
+ lines.push(`${indent}${st}`);
1565
+ lines.push(`${indent}if ${cond.expr} {`);
1566
+ // TAKEN = then: only this branch's stmts run (short-circuit ≡ evaluate).
1567
+ for (const st of thenC.stmts)
1568
+ lines.push(`${indent}\t${st}`);
1569
+ lines.push(`${indent}\t${typedLocal(node.id)} = ${thenC.expr}`);
1570
+ lines.push(`${indent}} else {`);
1571
+ for (const st of elseC.stmts)
1572
+ lines.push(`${indent}\t${st}`);
1573
+ lines.push(`${indent}\t${typedLocal(node.id)} = ${elseC.expr}`);
1574
+ lines.push(`${indent}}`);
1575
+ lines.push(`${indent}produced_${s} = true`);
1576
+ void nodeRef;
1577
+ for (const cl of closers)
1578
+ lines.push(cl);
1579
+ return lines.join("\n");
1580
+ }
1320
1581
  /**
1321
1582
  * emitProducedAwareValue — the #86 part 1 produced-aware output lowering (go). It mirrors the
1322
1583
  * boxed-typed emitTypedValue, but a `ref`/`refOpt` whose HEAD is a skippable relationSingle child is
@@ -1433,9 +1694,190 @@ function goZero(ref) {
1433
1694
  // bc#90 / runtime-free: the nv* helper set (nvVE/nvBindVE/nvFieldVE/nvCatVE/nvArrVE) is REMOVED —
1434
1695
  // those were dslcontracts.Value-based; the covered plane now lowers every port to a native Go value
1435
1696
  // (emitPortInit) so no VE helper is emitted, and the module imports ZERO bc runtime.
1697
+ // ── native expression compiler backend (#108) — cond `if` / map `when` / cond branches ──────────
1698
+ //
1699
+ // The runtime-free native expression compiler (native-expr.ts) mirrors runBehavior's cond/guard
1700
+ // Expression evaluation with BYTE-EQUAL semantics. The value-dependent runtime failures (INT_OVERFLOW
1701
+ // / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS / INVALID_LITERAL for a huge len) are emitted as calls to a
1702
+ // FIXED set of RUNTIME-FREE helper funcs baked into the module (bcExpr* below) — the covered module
1703
+ // still imports ZERO bc runtime. A fallible helper returns (T, *BehaviorError); the runner early-returns
1704
+ // the failure (its zero output value is `zeroOut`).
1705
+ let goExprTempSeq = 0;
1706
+ /** module-level accumulator: set true when the native-expr compiler emits a fallible helper call
1707
+ * (so emit() includes the helper library + imports `math`). Reset at the start of each emit(). */
1708
+ const goExprUsed = { helpers: false };
1709
+ /** GoNativeExprBackend — renders native-expr for Go. `zeroOut` is the runner's error-return zero
1710
+ * value; `resolveHead` maps a ref head (input port / input-array $as elem / prior node) to a typed
1711
+ * base expr. Sets goExprUsed.helpers when a fallible helper is emitted (so emit() bakes the helper lib). */
1712
+ function makeGoExprBackend(zeroOut, resolveHead, plan) {
1713
+ const used = goExprUsed;
1714
+ return {
1715
+ lang: "go",
1716
+ freshTemp: () => `ex_${goExprTempSeq++}`,
1717
+ renderType: (ref) => renderTypeRef(ref),
1718
+ scalarType: (s) => goScalar(s),
1719
+ strLit: (s) => JSON.stringify(s),
1720
+ boolLit: (b) => (b ? "true" : "false"),
1721
+ intLit: (dec) => `int64(${dec})`,
1722
+ floatLit: (n) => `float64(${goFloatLitExpr(n)})`,
1723
+ fieldAccess: (baseExpr, baseRef, path) => goTypedInternals.typedFieldAccess(baseExpr, baseRef, path, plan),
1724
+ resolveHead,
1725
+ hoistFallible: (temp, _ty, call) => {
1726
+ used.helpers = true;
1727
+ // a unique err var per hoist (Go `:=` re-declares fine in the same block only for NEW LHS; use a
1728
+ // fresh err name to avoid "no new variables on left side of :=" when several hoists share a block).
1729
+ const errv = `err_${goExprTempSeq++}`;
1730
+ return [`${temp}, ${errv} := ${call}`, `if ${errv} != nil {`, `\treturn ${zeroOut}, ${errv}`, `}`];
1731
+ },
1732
+ helperCall: (helper, args) => {
1733
+ used.helpers = true;
1734
+ return `bcExpr_${helper}(${args.join(", ")})`;
1735
+ },
1736
+ binOp: (op, a, b) => `(${a} ${op} ${b})`,
1737
+ concat: (parts) => {
1738
+ // native string + concat (all parts are native strings) — no import needed.
1739
+ return `(${parts.join(" + ")})`;
1740
+ },
1741
+ notOp: (a) => `(!${a})`,
1742
+ structLit: (name, inits) => `${name}{${inits.map((i) => `${goFieldName(i.field)}: ${i.expr}`).join(", ")}}`,
1743
+ arrLit: (elemTy, items) => `[]${elemTy}{${items.join(", ")}}`,
1744
+ ternary: (cond, t, e, ty) => `func() ${ty} { if ${cond} { return ${t} }; return ${e} }()`,
1745
+ shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
1746
+ // and: temp=false; if left { <rStmts>; temp=rExpr }. or: temp=true; if !left { <rStmts>; temp=rExpr }.
1747
+ const gate = op === "and" ? left : `!(${left})`;
1748
+ const seed = op === "and" ? "false" : "true";
1749
+ return [
1750
+ `var ${temp} bool = ${seed}`,
1751
+ `if ${gate} {`,
1752
+ ...rStmts.map((s) => `\t${s}`),
1753
+ `\t${temp} = ${rExpr}`,
1754
+ `}`,
1755
+ ];
1756
+ },
1757
+ condGuard: (temp, ty, cond, tStmts, tExpr, eStmts, eExpr) => [
1758
+ `var ${temp} ${ty}`,
1759
+ `if ${cond} {`,
1760
+ ...tStmts.map((s) => `\t${s}`),
1761
+ `\t${temp} = ${tExpr}`,
1762
+ `} else {`,
1763
+ ...eStmts.map((s) => `\t${s}`),
1764
+ `\t${temp} = ${eExpr}`,
1765
+ `}`,
1766
+ ],
1767
+ };
1768
+ }
1769
+ /** Go float literal (finite; matches goFloatLit convention used elsewhere). */
1770
+ function goFloatLitExpr(n) {
1771
+ if (Number.isInteger(n))
1772
+ return `${n}.0`;
1773
+ return String(n);
1774
+ }
1775
+ /**
1776
+ * emitExprHelpers — the FIXED runtime-free native-expr helper library (Go). Emitted once per covered
1777
+ * module that uses any cond/guard/arith. Each helper is BYTE-EQUAL to the reference core in
1778
+ * expr-eval.ts (checkI64 / checkFinite / mod trunc / div widen / code-point compare / lenCore). All
1779
+ * failures are the LOCAL *BehaviorError with the SAME code strings as run_behavior.
1780
+ */
1781
+ function emitExprHelpers() {
1782
+ return `// ── native-expr runtime-free helper library (#108) — byte-equal to expr-eval.ts core ──
1783
+ const bcI64Max int64 = 9223372036854775807
1784
+ const bcI64Min int64 = -9223372036854775808
1785
+
1786
+ func bcExpr_addI64(a, b int64) (int64, *BehaviorError) {
1787
+ r := a + b
1788
+ // signed overflow detection (a+b): same-sign operands, result flips sign.
1789
+ if (a > 0 && b > 0 && r < 0) || (a < 0 && b < 0 && r > 0) {
1790
+ return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
1791
+ }
1792
+ return r, nil
1793
+ }
1794
+ func bcExpr_subI64(a, b int64) (int64, *BehaviorError) {
1795
+ r := a - b
1796
+ if (a >= 0 && b < 0 && r < 0) || (a < 0 && b > 0 && r >= 0) {
1797
+ return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
1798
+ }
1799
+ return r, nil
1800
+ }
1801
+ func bcExpr_mulI64(a, b int64) (int64, *BehaviorError) {
1802
+ if a == 0 || b == 0 {
1803
+ return 0, nil
1804
+ }
1805
+ r := a * b
1806
+ if r/b != a || (a == bcI64Min && b == -1) {
1807
+ return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
1808
+ }
1809
+ return r, nil
1810
+ }
1811
+ func bcExpr_negI64(a int64) (int64, *BehaviorError) {
1812
+ if a == bcI64Min {
1813
+ return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
1814
+ }
1815
+ return -a, nil
1816
+ }
1817
+ func bcExpr_checkFinite(v float64) (float64, *BehaviorError) {
1818
+ if v != v || v > 1.7976931348623157e308 || v < -1.7976931348623157e308 {
1819
+ return 0, &BehaviorError{Code: "NAN_OR_INF", Message: "non-finite float"}
1820
+ }
1821
+ return v, nil
1822
+ }
1823
+ func bcExpr_addF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a + b) }
1824
+ func bcExpr_subF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a - b) }
1825
+ func bcExpr_mulF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a * b) }
1826
+ func bcExpr_negF(a float64) (float64, *BehaviorError) { return bcExpr_checkFinite(-a) }
1827
+ func bcExpr_modF(a, b float64) (float64, *BehaviorError) { m := math.Mod(a, b); return bcExpr_checkFinite(m) }
1828
+ func bcExpr_modI64(a, b int64) (int64, *BehaviorError) {
1829
+ if b == 0 {
1830
+ return 0, &BehaviorError{Code: "MOD_ZERO", Message: "int mod by zero"}
1831
+ }
1832
+ // Go % is truncated (sign of dividend) — matches modCore.
1833
+ r := a % b
1834
+ // overflow only at MinInt64 % -1 → 0 in Go (no overflow); still checkI64-safe.
1835
+ return r, nil
1836
+ }
1837
+ func bcExpr_widenI64F(a int64) (float64, *BehaviorError) {
1838
+ // div widening exact range is ±2^53 (WIDEN_EXACT). Outside → PRECISION_LOSS.
1839
+ if a > 9007199254740992 || a < -9007199254740992 {
1840
+ return 0, &BehaviorError{Code: "PRECISION_LOSS", Message: "int exceeds exact float range (±2^53)"}
1841
+ }
1842
+ return float64(a), nil
1843
+ }
1844
+ func bcExpr_divF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a / b) }
1845
+ func bcExpr_lenArr[T any](a []T) (int64, *BehaviorError) {
1846
+ return int64(len(a)), nil
1847
+ }
1848
+ // code-point (UTF-8 rune) order comparison — matches cmpCodePoints (NOT UTF-16 / byte compare, which
1849
+ // Go's native string < already is for valid UTF-8, but we compare rune-by-rune to match exactly).
1850
+ func bcExpr_cmpCP(a, b string) int {
1851
+ ra := []rune(a)
1852
+ rb := []rune(b)
1853
+ n := len(ra)
1854
+ if len(rb) < n {
1855
+ n = len(rb)
1856
+ }
1857
+ for i := 0; i < n; i++ {
1858
+ if ra[i] != rb[i] {
1859
+ if ra[i] < rb[i] {
1860
+ return -1
1861
+ }
1862
+ return 1
1863
+ }
1864
+ }
1865
+ if len(ra) == len(rb) {
1866
+ return 0
1867
+ }
1868
+ if len(ra) < len(rb) {
1869
+ return -1
1870
+ }
1871
+ return 1
1872
+ }
1873
+ func bcExpr_strLt(a, b string) bool { return bcExpr_cmpCP(a, b) < 0 }
1874
+ func bcExpr_strLe(a, b string) bool { return bcExpr_cmpCP(a, b) <= 0 }
1875
+ func bcExpr_strGt(a, b string) bool { return bcExpr_cmpCP(a, b) > 0 }
1876
+ func bcExpr_strGe(a, b string) bool { return bcExpr_cmpCP(a, b) >= 0 }`;
1877
+ }
1436
1878
  // ── BindNativeRaw dispatch surface ──────────────────────────────────────────────────────
1437
1879
  /** The struct dispatch surface: ComponentNamesNativeRaw + the per-component struct runners
1438
- * (run_native_raw_struct_<comp>) ARE the exposed API. The consumer calls the struct runner directly
1880
+ * (RunNativeRawStruct_<comp>) ARE the exposed API. The consumer calls the struct runner directly
1439
1881
  * (it returns the typed model — NO Value serialization on the read hot path, matching how graphddb
1440
1882
  * reads: a model, never a boxed Value). There is deliberately NO Value dispatch map — that would
1441
1883
  * require a Value serializer (the boxing residue bc#77 eliminates). A canonical Value observation
@@ -1443,7 +1885,7 @@ function goZero(ref) {
1443
1885
  * which lives OUTSIDE this module so the covered module stays literal whole-file zero. */
1444
1886
  function emitStructSurface(native) {
1445
1887
  return `// ComponentNamesNativeRaw — covered reads exposed on the combined struct-native path (declaration
1446
- // order). Each is driven via run_native_raw_struct_<comp>(handlers, input) -> (T, error): a STRUCT
1888
+ // order). Each is driven via RunNativeRawStruct_<comp>(handlers, input) -> (T, error): a STRUCT
1447
1889
  // return; the consumer keeps the model native (no Value serialization on the read hot path — the
1448
1890
  // boxing residue bc#77 removes). See INTEGRATION.md §6.
1449
1891
  var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name)).join(", ")}}`;
@@ -1453,11 +1895,13 @@ var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name
1453
1895
  * mismatch Sprintf, `strings` for a concat builder, `sync` for a parallel stage). ExpectedSpecVersions
1454
1896
  * stays as a local `var` for provenance, but the old `dslcontracts.SpecVersions` skew `init()` is GONE
1455
1897
  * (change #5 — there is no runtime to skew against once the module is runtime-free). */
1456
- function emitMinimalHeader(ctx, needStrings, needSync, needFmt) {
1898
+ function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath) {
1457
1899
  const sv = ctx.specVersions;
1458
1900
  const imports = [];
1459
1901
  if (needFmt)
1460
1902
  imports.push(`\t"fmt"`);
1903
+ if (needMath)
1904
+ imports.push(`\t"math"`);
1461
1905
  if (needStrings)
1462
1906
  imports.push(`\t"strings"`);
1463
1907
  if (needSync)
@@ -1484,6 +1928,9 @@ function emit(ctx) {
1484
1928
  const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
1485
1929
  const plan = buildTypePlan(ctx.ir);
1486
1930
  const decls = emitTypeDecls(plan);
1931
+ // #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler when it emits
1932
+ // a fallible checked-arith helper call — determines whether the helper library + `math` import ship).
1933
+ goExprUsed.helpers = false;
1487
1934
  // Emit the runners first so the nv* helper usage flags are known.
1488
1935
  const flags = { bind: false, field: false, cat: false, arr: false };
1489
1936
  const structs = [];
@@ -1493,18 +1940,23 @@ function emit(ctx) {
1493
1940
  const elemCopiers = [];
1494
1941
  const runnerCodes = [];
1495
1942
  for (const c of native) {
1496
- inStructs.push(emitInStruct(c));
1943
+ inStructs.push(emitInStruct(c, plan));
1497
1944
  const typedNodes = new Map();
1945
+ // build the full typed-node map FIRST so a map's over-element resolution can see every node.
1946
+ for (const n of c.body)
1947
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1498
1948
  for (const n of c.body) {
1499
1949
  if ("map" in n) {
1500
- // #86 pt2: a map node emits an element ports struct + a batch struct (Items []Value of the
1501
- // per-element native ports structs; no key-value object in the accessor).
1502
- structs.push(emitMapPortsStructs(c, n));
1950
+ // #86 pt2 / #108: a map node emits an element ports struct + a batch struct. The element ports
1951
+ // may read the `$as` binding resolved via the over-element type (prior node / input array).
1952
+ structs.push(emitMapPortsStructs(c, n, typedNodes, plan));
1953
+ }
1954
+ else if ("cond" in n) {
1955
+ // #108: a cond node has NO ports struct (it is a pure Expression — no handler dispatch).
1503
1956
  }
1504
1957
  else {
1505
- structs.push(emitPortsStruct(c, n));
1958
+ structs.push(emitPortsStruct(c, n, undefined, plan));
1506
1959
  }
1507
- typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1508
1960
  }
1509
1961
  // CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
1510
1962
  rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
@@ -1539,7 +1991,10 @@ function emit(ctx) {
1539
1991
  `box uncovered shapes. Implement native coverage for this shape, fix the spec if the shape is ` +
1540
1992
  `genuinely runtime-dynamic, or generate this component on the --ir dynamic interpreter surface.`);
1541
1993
  }
1542
- const header = emitMinimalHeader(ctx, false, needSync, needFmt);
1994
+ // #108: `math` is imported iff the native-expr compiler emitted a fallible helper (bcExpr_modF uses
1995
+ // math.Mod). The helper library itself is baked into the module only when used (goExprUsed.helpers).
1996
+ const needMath = goExprUsed.helpers;
1997
+ const header = emitMinimalHeader(ctx, false, needSync, needFmt, needMath);
1543
1998
  const banner = `// COMBINED READ layer (bc#90 — the RUNTIME-FREE read de-box; STRUCT-ONLY, FULLY-NATIVE surface). A
1544
1999
  // covered read is a strictly-sequential typed componentRef chain (point reads + relationKind:single
1545
2000
  // children), emitted ONLY here: each node builds a native ports struct by direct field assignment (every
@@ -1551,7 +2006,7 @@ function emit(ctx) {
1551
2006
  // *BehaviorError (same codes, byte-equal to run_behavior). Exec is INLINE sequential (no plan driver) so
1552
2007
  // a relation child reads the parent's REAL struct result via direct field access and the child-present
1553
2008
  // decision is made from the real parent value — relationSingle / connection converge with run_behavior
1554
- // (fixes #323/#74). The exposed API is the STRUCT-returning run_native_raw_struct_<comp> (the consumer
2009
+ // (fixes #323/#74). The exposed API is the STRUCT-returning RunNativeRawStruct_<comp> (the consumer
1555
2010
  // keeps the model native — no serialization on the read hot path). This module is FULLY NATIVE: a naive
1556
2011
  // grep for boxing primitives OR for the bc-runtime package finds nothing, by design (bc#90/runtime-free).`;
1557
2012
  const sections = [
@@ -1559,12 +2014,19 @@ function emit(ctx) {
1559
2014
  `// Local concrete failure type (runtime-free) — a covered runner returns *BehaviorError (satisfies\n// error) instead of a bc-runtime failure, so the fully-covered module imports ZERO bc runtime.\n${behaviorErrorType}`,
1560
2015
  decls ? `// Typed struct declarations (outType-derived; hash-dedup — shared type plan).\n${decls}` : undefined,
1561
2016
  `// Per-component CONCRETE input structs (fields = inputPorts; the consumer builds them natively —\n// no generic *Obj crosses the covered read boundary).\n${inStructs.join("\n\n")}`,
1562
- `// CONCRETE per-node handler-result row structs (typed fields = the node's outType; + error signal).\n${rowStructs.join("\n\n")}`,
2017
+ rowStructs.some((r) => r.trim().length > 0)
2018
+ ? `// CONCRETE per-node handler-result row structs (typed fields = the node's outType; + error signal).\n${rowStructs.filter((r) => r.trim().length > 0).join("\n\n")}`
2019
+ : undefined,
1563
2020
  `// Per-component CONCRETE handler interfaces (one typed Node_* method per covered node).\n${handlerIfaces.join("\n\n")}`,
1564
2021
  elemCopiers.length
1565
2022
  ? `// Augmented map-element copiers (over element fields + the concrete into row — pure assignment).\n${elemCopiers.join("\n\n")}`
1566
2023
  : undefined,
1567
- `// Native ports structs (one per componentRef node; typed per the static port type).\n${structs.join("\n\n")}`,
2024
+ structs.some((r) => r.trim().length > 0)
2025
+ ? `// Native ports structs (one per componentRef node; typed per the static port type).\n${structs.filter((r) => r.trim().length > 0).join("\n\n")}`
2026
+ : undefined,
2027
+ goExprUsed.helpers
2028
+ ? `// #108 native-expr helper library (runtime-free; byte-equal to expr-eval.ts core) — used by\n// cond / map-guard checked arithmetic. NO bc-runtime reference (failures use the LOCAL *BehaviorError).\n${emitExprHelpers()}`
2029
+ : undefined,
1568
2030
  `// Combined read runners (STRUCT-returning — the fully de-plumbed path).\n${runnerCodes.join("\n\n")}`,
1569
2031
  structSurface,
1570
2032
  ].filter((s) => !!s);
@@ -1595,12 +2057,12 @@ export function goTypedNativeObserve(ir, runtimeImport) {
1595
2057
  // decode the generic *Obj input into the CONCRETE In_<comp> struct (test glue — ONE decode at the
1596
2058
  // top, OUTSIDE the covered hot path; a REQUIRED declared input is present so an absent key is a
1597
2059
  // fail-closed decode error surfaced here, never on the native runner path).
1598
- return `\tif name == ${JSON.stringify(c.name)} {\n\t\tin, ierr := decode_${inStructName(c.name)}(input)\n\t\tif ierr != nil {\n\t\t\treturn nil, ierr\n\t\t}\n\t\ttv, err := run_native_raw_struct_${sanitize(c.name)}(handlers, in)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn ${ser}, nil\n\t}`;
2060
+ return `\tif name == ${JSON.stringify(c.name)} {\n\t\tin, ierr := decode_${inStructName(c.name)}(input)\n\t\tif ierr != nil {\n\t\t\treturn nil, ierr\n\t\t}\n\t\ttv, err := ${runnerName(c.name)}(handlers, in)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn ${ser}, nil\n\t}`;
1599
2061
  })
1600
2062
  .join("\n");
1601
2063
  // input decoders (TEST glue): generic *Obj -> concrete In_<comp>. Scalar fields read the typed value
1602
2064
  // (a `.(T)` here is on the SDK/wire Value in the OBSERVE companion, never in the covered module).
1603
- const inDecoders = native.map((c) => emitInDecoder(c)).join("\n\n");
2065
+ const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
1604
2066
  // The observe companion drives EVERY covered component, so its handler type param H must satisfy
1605
2067
  // EVERY per-component concrete Handler_<comp> interface — a combined constraint embeds them all.
1606
2068
  const combinedConstraint = `allNativeRawHandlers`;
@@ -1621,6 +2083,8 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
1621
2083
  for (const n of c.body)
1622
2084
  typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1623
2085
  for (const n of c.body) {
2086
+ if ("cond" in n)
2087
+ continue; // #108: cond has no handler method (pure Expression) — no scripted impl.
1624
2088
  const ref = typedNodes.get(n.id);
1625
2089
  if ("map" in n) {
1626
2090
  const m = n.map;
@@ -1788,7 +2252,7 @@ ${constraintDecl}
1788
2252
  ${methodImpls.join("\n\n")}
1789
2253
 
1790
2254
  // ObserveNativeRaw drives the struct-native runner and serializes to a canonical Value (test only).
1791
- // GENERIC over the CONCRETE combined handler type H — so each enclosed run_native_raw_struct_* call is
2255
+ // GENERIC over the CONCRETE combined handler type H — so each enclosed RunNativeRawStruct_* call is
1792
2256
  // instantiated at the concrete handler type (direct, devirtualized per-node method calls), not at an
1793
2257
  // interface. Instantiate with your concrete handler type to preserve the covered path's concreteness.
1794
2258
  func ObserveNativeRaw[H ${combinedConstraint}](name string, handlers H, input *${PKG}.Obj) (${PKG}.Value, error) {