behavior-contracts 0.5.0 → 0.6.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 +495 -75
  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 +553 -103
  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,13 @@ 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) {
106
123
  if (staticStringArrElems(node) !== null)
107
124
  return "[]string";
108
125
  const num = numLiteralGoExpr(node);
109
126
  if (num !== null)
110
127
  return Number.isInteger(node) ? "int64" : "float64";
111
- const ft = inferPortType(node, inputPorts);
128
+ const ft = inferPortType(node, inputPorts, asBinding);
112
129
  if (ft === "Value") {
113
130
  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
131
  }
@@ -236,27 +253,35 @@ function isNativeRaw(comp) {
236
253
  * element struct by copying the over element's typed fields + materializing the into field from the
237
254
  * aligned batch raw — NO boxed Value on any plane.
238
255
  */
239
- function coveredMapNode(n, bodyIds) {
256
+ function coveredMapNode(n, bodyIds, comp) {
240
257
  if (n === null || typeof n !== "object" || !("map" in n))
241
258
  return false;
242
259
  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)
260
+ // #86 pt2: batched map...into. #93 shape 2/3: (non-)batched map...into / connection-collect. #108:
261
+ // a guarded map (`when`) IS covered when it has NO `into` (guard+into is heterogeneous — skipped
262
+ // elements lack the into field, cannot de-box into one []T; keep it fail-closed, matching the typed
263
+ // straightline reject). A guarded map over a native-typed element collection drops skipped elements.
264
+ if (m.when !== undefined && m.into !== undefined)
248
265
  return false;
249
266
  if (m.policy !== undefined && m.policy !== "fail")
250
267
  return false;
251
268
  if (m.relationKind !== undefined && m.relationKind !== "single" && m.relationKind !== "connection")
252
269
  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.
270
+ // `over` element type must be STATICALLY resolvable: a ref to a PRIOR BODY NODE's typed arr field
271
+ // (mapOverElemType) OR a ref to an INPUT array port that carries `elemType` (#108 the C3 "BC does
272
+ // not infer" rule: an input array WITHOUT elemType stays fail-closed / uncovered).
257
273
  const overE = classifyExpr(m.over);
258
- if (overE.kind !== "ref" || !bodyIds.has(overE.path[0]))
274
+ if (overE.kind !== "ref")
259
275
  return false;
276
+ const head = overE.path[0];
277
+ if (!bodyIds.has(head)) {
278
+ // input-array over: covered only when the input port declares elemType (#108).
279
+ if (overE.path.length !== 1)
280
+ return false;
281
+ const schema = comp.inputPorts?.[head];
282
+ if (schema === undefined || schema.elemType === undefined)
283
+ return false;
284
+ }
260
285
  const ports = m.ports;
261
286
  for (const p of Object.keys(ports))
262
287
  if (!portIsStatic(ports[p]))
@@ -295,13 +320,21 @@ function coverageRejectReason(comp) {
295
320
  }
296
321
  const bodyIds = new Set(comp.body.map((b) => b.id));
297
322
  for (const n of comp.body) {
298
- if ("cond" in n)
299
- return `node '${n.id}' is a cond branch (not yet native-covered)`;
323
+ if ("cond" in n) {
324
+ // #108: a cond node is native-covered its `if`/`then`/`else` compile to native expressions via
325
+ // the runtime-free NativeExprCompiler (byte-equal to run_behavior's cond lower). If an expression
326
+ // is not statically compilable (uncovered op / refOpt / opt operand / type mismatch), the compiler
327
+ // fails closed at emit (a loud GeneratorFailure) — never a silent box. cond requires outType.
328
+ if (n.outType === undefined)
329
+ return `node '${n.id}' cond has no outType (required to de-box the branch join)`;
330
+ continue;
331
+ }
300
332
  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)`;
333
+ // #86 pt2 batched map...into / #93 shape 2/3 / #108 guarded map (no into) / map-over-input (with
334
+ // elemType) ARE covered. A non-covered map shape (guard+into heterogeneous / over-input without
335
+ // elemType) falls through to the loud fail-closed reject.
336
+ if (!coveredMapNode(n, bodyIds, comp))
337
+ return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
305
338
  continue;
306
339
  }
307
340
  if (!("component" in n))
@@ -342,13 +375,13 @@ function assertTypedCoverage(comp) {
342
375
  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
376
  }
344
377
  // ── native ports struct + PortReader ──────────────────────────────────────────────────
345
- function emitPortsStruct(comp, node) {
378
+ function emitPortsStruct(comp, node, asBinding) {
346
379
  const structName = portsStructName(comp.name, node.id);
347
380
  const portNames = Object.keys(node.ports);
348
381
  if (portNames.length === 0) {
349
382
  return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
350
383
  }
351
- const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp.inputPorts));
384
+ const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding));
352
385
  const fieldNames = portNames.map(goPortFieldName);
353
386
  const fields = portNames
354
387
  .map((p, i) => `\t${fieldNames[i]} ${fieldTypes[i]} // ${JSON.stringify(p)}`)
@@ -391,6 +424,14 @@ function rawElemStructName(compName, nodeId) {
391
424
  function handlerIfaceName(compName) {
392
425
  return `Handler_${sanitize(compName)}`;
393
426
  }
427
+ /** RunNativeRawStruct_<comp> — the EXPORTED name of the per-component struct-returning combined runner
428
+ * (bc#102). It MUST be an exported Go identifier so a consumer in a DIFFERENT package can call it (a
429
+ * leading-lowercase name is unexported / uncallable cross-package). The `RunNativeRawStruct_` prefix
430
+ * guarantees the leading char is uppercase regardless of how the component name sanitizes, and the
431
+ * sanitized component suffix keeps it unique per covered component. */
432
+ function runnerName(compName) {
433
+ return `RunNativeRawStruct_${sanitize(compName)}`;
434
+ }
394
435
  /** Node_<comp>_<nodeId> — the concrete per-node handler method name. */
395
436
  function nodeMethodName(compName, nodeId) {
396
437
  return `Node_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
@@ -404,6 +445,9 @@ function nodeMethodName(compName, nodeId) {
404
445
  function emitRawRowStructs(comp, typedNodes, plan) {
405
446
  const parts = [];
406
447
  for (const n of comp.body) {
448
+ // #108: a cond node has NO handler-result row (pure Expression — no dispatch).
449
+ if ("cond" in n)
450
+ continue;
407
451
  const ref = typedNodes.get(n.id);
408
452
  if ("map" in n) {
409
453
  // a covered map node: RawElem_<comp>_<node> is the HANDLER'S per-element return shape:
@@ -498,6 +542,8 @@ function emitHandlerInterface(comp, typedNodes, plan) {
498
542
  lines.push(`// Node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
499
543
  lines.push(`type ${handlerIfaceName(comp.name)} interface {`);
500
544
  for (const n of comp.body) {
545
+ if ("cond" in n)
546
+ continue; // #108: a cond node has no handler method (pure Expression).
501
547
  const method = nodeMethodName(comp.name, n.id);
502
548
  const bt = boundGoType(n, comp, typedNodes, plan);
503
549
  if ("map" in n) {
@@ -524,10 +570,15 @@ function mapElemMaterializerName(compName, nodeId) {
524
570
  * slice of the concrete per-element ports structs (NO `[]dslcontracts.Value`, NO PortReader accessor);
525
571
  * the consumer reads `bp.Items[i].F_PK` directly.
526
572
  */
527
- function emitMapPortsStructs(comp, node) {
573
+ function emitMapPortsStructs(comp, node, typedNodes, plan) {
528
574
  const m = node.map;
529
575
  const structName = portsStructName(comp.name, node.id);
530
- const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports });
576
+ // #108: the element ports may read the `$as` binding ($id / $g.field) resolve the over element
577
+ // type so those ports type their struct field natively (map-over-input scalar element / prior-node
578
+ // struct element). A pure-static/concat port ignores the binding.
579
+ const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
580
+ const asBinding = { name: m.as, ref: elemRef, plan };
581
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding);
531
582
  const batchStruct = `${structName}_batch`;
532
583
  return `${elemStruct}
533
584
 
@@ -596,19 +647,35 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
596
647
  }
597
648
  return parts.join("\n\n");
598
649
  }
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) {
650
+ /**
651
+ * mapOverElemInfo resolve a covered map's `over` collection: its element TypeRef + the Go expression
652
+ * that yields the native over slice. Two sources (#108):
653
+ * - a ref to a PRIOR BODY NODE's typed arr field (`t_root.Groups`) — element type from the parent struct;
654
+ * - a ref to an INPUT ARRAY port carrying `elemType` (`in.Ids`) — element type from PortSchema.elemType.
655
+ * A map over an input array WITHOUT elemType is fail-closed (BC does not infer — consumer-interface C3).
656
+ */
657
+ function mapOverElemInfo(comp, node, typedNodes, plan) {
603
658
  const m = node.map;
604
659
  const e = classifyExpr(m.over);
605
660
  if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
606
661
  const baseRef = typedNodes.get(e.path[0]);
607
- const { ref } = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
662
+ const { expr, ref } = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
608
663
  if (ref.kind === "arr")
609
- return renderTypeRef(ref.elem);
664
+ return { elemRef: ref.elem, overExpr: expr };
665
+ }
666
+ // input-array over (#108): the head is an input port; its declared elemType is the element type.
667
+ if (e.kind === "ref" && e.path.length === 1) {
668
+ const schema = comp.inputPorts?.[e.path[0]];
669
+ const et = schema?.elemType;
670
+ if (et !== undefined) {
671
+ return { elemRef: deriveTypeRef(et, plan), overExpr: `in.${goFieldName(e.path[0])}` };
672
+ }
610
673
  }
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)}).`);
674
+ 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).`);
675
+ }
676
+ /** The Go type of the over-element (back-compat shim). */
677
+ function mapOverElemType(comp, node, typedNodes, plan) {
678
+ return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
612
679
  }
613
680
  function classifyPort(node) {
614
681
  if (staticArrElems(node) !== null)
@@ -663,8 +730,8 @@ function reconstructG(e) {
663
730
  function inStructName(compName) {
664
731
  return `In_${sanitize(compName)}`;
665
732
  }
666
- /** Go type for an input port's declared type (scalar → concrete; anything else Value). */
667
- function inputPortGoType(schema) {
733
+ /** Go type for an input port's declared type (scalar → concrete; array+elemType → []ElemT; else Value). */
734
+ function inputPortGoType(schema, plan) {
668
735
  switch (schema?.type) {
669
736
  case "string":
670
737
  return "string";
@@ -674,6 +741,15 @@ function inputPortGoType(schema) {
674
741
  return "float64";
675
742
  case "bool":
676
743
  return "bool";
744
+ case "array":
745
+ case "arr": {
746
+ // #108: an input ARRAY port with a declared elemType lowers to a native []ElemT (the map-over-input
747
+ // $as element type source). Without elemType or a plan, it stays a boxed Value (uncovered / fail-closed).
748
+ const et = schema.elemType;
749
+ if (et !== undefined && plan !== undefined)
750
+ return `[]${renderTypeRef(deriveTypeRef(et, plan))}`;
751
+ return `${PKG}.Value`;
752
+ }
677
753
  default:
678
754
  return `${PKG}.Value`;
679
755
  }
@@ -695,14 +771,14 @@ function inputScalarKind(schema) {
695
771
  }
696
772
  /** emitInStruct — the concrete input struct declaration for a covered component (fields = inputPorts,
697
773
  * declaration order = Object key order; gofmt-aligned). Empty inputPorts => empty struct. */
698
- function emitInStruct(comp) {
774
+ function emitInStruct(comp, plan) {
699
775
  const name = inStructName(comp.name);
700
776
  const ports = Object.keys(comp.inputPorts ?? {});
701
777
  if (ports.length === 0) {
702
778
  return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
703
779
  }
704
780
  const names = ports.map((p) => goFieldName(p));
705
- const types = ports.map((p) => inputPortGoType(comp.inputPorts[p]));
781
+ const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan));
706
782
  const nameW = Math.max(0, ...names.map((n) => n.length));
707
783
  const typeW = Math.max(0, ...types.map((t) => t.length));
708
784
  const fields = ports
@@ -722,7 +798,7 @@ ${fields}
722
798
  * pre-built by the consumer. A missing key leaves the zero value (every covered input is required +
723
799
  * always supplied by the corpus); a present-but-wrong-scalar is a fail-closed decode error.
724
800
  */
725
- function emitInDecoder(comp) {
801
+ function emitInDecoder(comp, plan) {
726
802
  const name = inStructName(comp.name);
727
803
  const ports = Object.keys(comp.inputPorts ?? {});
728
804
  const lines = [];
@@ -739,8 +815,28 @@ function emitInDecoder(comp) {
739
815
  lines.push(`\t}`);
740
816
  for (const p of ports) {
741
817
  const field = goFieldName(p);
742
- const scalar = inputScalarKind(comp.inputPorts[p]);
818
+ const schema = comp.inputPorts[p];
819
+ const scalar = inputScalarKind(schema);
820
+ const et = schema.elemType;
743
821
  lines.push(`\tif v, ok := input.Get(${JSON.stringify(p)}); ok {`);
822
+ if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
823
+ // #108: decode the boxed Value array into the native []ElemT (test glue — materialize each element
824
+ // via the typed materializer; a non-array or non-conforming element is a fail-closed decode error).
825
+ const elemRef = deriveTypeRef(et, plan);
826
+ const elemGo = renderTypeRef(elemRef);
827
+ lines.push(`\t\tarr, isArr := v.([]${PKG}.Value)`);
828
+ lines.push(`\t\tif !isArr {`);
829
+ lines.push(`\t\t\treturn in, ${PKG}.NewExprFailure("TYPE_MISMATCH", "input ${p}: expected array, got "+${PKG}.TypeName(v))`);
830
+ lines.push(`\t\t}`);
831
+ lines.push(`\t\tdec := make([]${elemGo}, 0, len(arr))`);
832
+ lines.push(`\t\tfor _, ev := range arr {`);
833
+ // materializeExpr returns the must*-based decode (panics on a malformed corpus element — test glue).
834
+ lines.push(`\t\t\tdec = append(dec, ${goTypedInternals.materializeExpr("ev", elemRef, plan)})`);
835
+ lines.push(`\t\t}`);
836
+ lines.push(`\t\tin.${field} = dec`);
837
+ lines.push(`\t}`);
838
+ continue;
839
+ }
744
840
  if (scalar === undefined) {
745
841
  lines.push(`\t\tin.${field} = v`);
746
842
  }
@@ -985,7 +1081,7 @@ function emitNativeRawRunner(comp, plan, flags) {
985
1081
  const outStructType = outRef ? renderTypeRef(outRef) : `${PKG}.Value`;
986
1082
  const zeroOut = outRef ? goZero(outRef) : "nil";
987
1083
  const lines = [];
988
- lines.push(`// run_native_raw_struct_${fn} — the STRUCT-RETURNING combined read (bc#77/#87): the fully`);
1084
+ lines.push(`// ${runnerName(comp.name)} — the STRUCT-RETURNING combined read (bc#77/#87): the fully`);
989
1085
  lines.push(`// de-plumbed path. Ports are a native struct (direct field assignment); the handler`);
990
1086
  lines.push(`// result is materialized straight into the node's outType struct. Node results are typed`);
991
1087
  lines.push(`// struct locals; a relation child reads the parent's REAL struct result via direct field`);
@@ -1001,7 +1097,7 @@ function emitNativeRawRunner(comp, plan, flags) {
1001
1097
  // whose Node_* methods return the concrete per-node row struct (typed fields = the node's outType).
1002
1098
  // The covered result plane carries no boxed ports / boxed value / dynamic accessor / semantic type-
1003
1099
  // 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) {`);
1100
+ lines.push(`func ${runnerName(comp.name)}[H ${handlerIfaceName(comp.name)}](handlers H, in ${inStructName(comp.name)}) (${outStructType}, error) {`);
1005
1101
  lines.push(`\t_ = in`);
1006
1102
  // resolveNativeRef for this runner: a ref head is either a PRIOR NODE (read its typed struct field
1007
1103
  // directly) or an INPUT PORT (read the typed `in.<Field>`). A REQUIRED scalar resolves to a native
@@ -1057,6 +1153,13 @@ function emitNativeRawRunner(comp, plan, flags) {
1057
1153
  lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut, flags));
1058
1154
  continue;
1059
1155
  }
1156
+ // #108: a cond node — pure Expression (no handler). Compile `if` to a native bool + materialize the
1157
+ // taken branch to the node's outType, assigning the node's typed cell. Byte-equal to run_behavior's
1158
+ // cond lower ({cond:[if,then,else]}, strict-bool, only the taken branch evaluated).
1159
+ if ("cond" in comp.body[i]) {
1160
+ lines.push(emitCondArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut));
1161
+ continue;
1162
+ }
1060
1163
  const node = comp.body[i];
1061
1164
  const meta = metas[i];
1062
1165
  const s = sanitize(node.id);
@@ -1174,22 +1277,37 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1174
1277
  const m = node.map;
1175
1278
  const batched = m.batched === true;
1176
1279
  const hasInto = m.into !== undefined;
1280
+ const hasGuard = m.when !== undefined;
1177
1281
  const s = sanitize(node.id);
1178
1282
  const structName = portsStructName(comp.name, node.id);
1179
1283
  const batchStruct = `${structName}_batch`;
1180
1284
  const arrRef = typedNodes.get(node.id);
1181
1285
  const elemGo = renderTypeRef(arrRef);
1182
1286
  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;
1287
+ // over collection element type + the native over-slice expression (#108: prior-node arr OR input array
1288
+ // with elemType). The $as element is a value of `overElemRef` (input-array element or prior-node elem).
1289
+ const { elemRef: overElemRef, overExpr } = mapOverElemInfo(comp, node, typedNodes, plan);
1290
+ const overElemGo = renderTypeRef(overElemRef);
1190
1291
  const elemMat = mapElemMaterializerName(comp.name, node.id);
1191
1292
  const priorHere = (head) => priorNodeAt(head, atPos);
1192
1293
  const portNames = Object.keys(m.ports);
1294
+ // #108 guard: compile the `when` expression to a native bool over an element-scoped resolveHead
1295
+ // ($as → the over element value `elemVar`; prior node → its typed local; input port → in.<Field>).
1296
+ // Returns the compiled {stmts, expr} or null (no guard). Mirrors run_behavior's per-element keep-gate
1297
+ // ({cond:[when,true,false]}, strict-bool). A non-bool / uncovered guard expr fails closed at emit.
1298
+ const mkGuard = (elemVar) => {
1299
+ if (!hasGuard)
1300
+ return null;
1301
+ const resolveHead = (head) => {
1302
+ if (head === asName)
1303
+ return { expr: elemVar, ref: overElemRef };
1304
+ if (priorHere(head))
1305
+ return { expr: typedLocal(head), ref: typedNodes.get(head) };
1306
+ return goInputHeadRef(head, comp, plan);
1307
+ };
1308
+ const be = makeGoExprBackend(zeroOut, resolveHead, plan);
1309
+ return new NativeExprCompiler(be).compileBool(m.when);
1310
+ };
1193
1311
  // emit the per-element native ports struct build (shared; `il` is the indent inside the element
1194
1312
  // loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
1195
1313
  const buildPorts = (il, elemVar) => {
@@ -1233,8 +1351,9 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1233
1351
  return null;
1234
1352
  return { expr, scalar: goScalar(acc.scalar) };
1235
1353
  };
1354
+ const asBinding = { name: asName, ref: overElemRef, plan };
1236
1355
  for (const pn of portNames) {
1237
- const fieldGoType = portFieldGoType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`);
1356
+ const fieldGoType = portFieldGoType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding);
1238
1357
  inits.push(emitPortInit(pn, m.ports[pn], fieldGoType, resolveRef));
1239
1358
  }
1240
1359
  out.push(`${il}ep_${s} := ${structName}{${inits.join(", ")}}`);
@@ -1254,41 +1373,69 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1254
1373
  lines.push(`${indent}over_${s} := ${overExpr}`);
1255
1374
  lines.push(`${indent}${typedLocal(node.id)} = make(${elemGo}, 0, len(over_${s}))`);
1256
1375
  const elemRowRef = mapElemRowRef(node, arrRef, plan);
1376
+ // emit the per-element guard keep-gate (#108). `il` = indent, `elemVar` = the over element loop var.
1377
+ // On skip: batched → `continue` (element excluded from items + keptOver); non-batched → `continue`.
1378
+ const emitGuard = (il, elemVar) => {
1379
+ const g = mkGuard(elemVar);
1380
+ if (g === null)
1381
+ return [];
1382
+ const out = [];
1383
+ for (const st of g.stmts)
1384
+ out.push(`${il}${st}`);
1385
+ out.push(`${il}if !(${g.expr}) {`);
1386
+ out.push(`${il}\tcontinue`);
1387
+ out.push(`${il}}`);
1388
+ return out;
1389
+ };
1257
1390
  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.
1391
+ // BATCHED (#86 pt2 into / #93 shape 3 no-into / #108 guarded no-into): build kept-element ports,
1392
+ // dispatch ONCE. The concrete Node_* returns RawRow{IsError,Err,Rows []RawElem} aligned to items.
1393
+ // With a guard, `items` (and, for into, `keptOver`) hold only kept elements — matching run_behavior's
1394
+ // keptIdx-aligned batch. `into` is never combined with a guard (rejected). no-into collects each row.
1260
1395
  lines.push(`${indent}items_${s} := make([]${structName}, 0, len(over_${s}))`);
1396
+ if (hasInto)
1397
+ lines.push(`${indent}keptOver_${s} := make([]${overElemGo}, 0, len(over_${s}))`);
1261
1398
  lines.push(`${indent}for _, ${elemLocal(s)} := range over_${s} {`);
1399
+ for (const l of emitGuard(indent + "\t", elemLocal(s)))
1400
+ lines.push(l);
1262
1401
  for (const l of buildPorts(indent + "\t", elemLocal(s)))
1263
1402
  lines.push(l);
1264
1403
  lines.push(`${indent}\titems_${s} = append(items_${s}, ep_${s})`);
1404
+ if (hasInto)
1405
+ lines.push(`${indent}\tkeptOver_${s} = append(keptOver_${s}, ${elemLocal(s)})`);
1265
1406
  lines.push(`${indent}}`);
1266
- // empty over => empty result (handler not called), matching run_behavior.
1407
+ // empty items => empty result (handler not called), matching run_behavior.
1267
1408
  lines.push(`${indent}if len(items_${s}) > 0 {`);
1268
1409
  lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: items_${s}}`);
1269
1410
  lines.push(`${indent}\tmo_${s}, mo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
1270
1411
  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
1412
  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
1413
  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
1414
  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}])`);
1415
+ // #86 pt2: zip-attach the into field onto each kept over element (augmented element copier).
1416
+ lines.push(`${indent}\tfor mk_${s} := range keptOver_${s} {`);
1417
+ lines.push(`${indent}\t\tel_${s} := ${elemMat}(keptOver_${s}[mk_${s}], mo_${s}.Rows[mk_${s}])`);
1418
+ lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1419
+ lines.push(`${indent}\t}`);
1277
1420
  }
1278
1421
  else {
1279
- // #93 shape 3: collect each element DIRECTLY from the concrete per-element row (field copy).
1422
+ // #93 shape 3 / #108: collect each element DIRECTLY from the aligned per-element row (field copy).
1423
+ lines.push(`${indent}\tfor mk_${s} := range mo_${s}.Rows {`);
1280
1424
  lines.push(`${indent}\t\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
1281
1425
  lines.push(emitRowCopy(`el_${s}`, `mo_${s}.Rows[mk_${s}]`, elemRowRef, plan, indent.length + 2));
1426
+ lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1427
+ lines.push(`${indent}\t}`);
1282
1428
  }
1283
- lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1284
- lines.push(`${indent}\t}`);
1285
1429
  lines.push(`${indent}}`);
1286
1430
  }
1287
1431
  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_.
1432
+ // #93 shape 2 / #108: NON-batched map — dispatch the child handler ONCE PER KEPT ELEMENT (N physical
1433
+ // requests, matching run_behavior's per-element loop). A guarded element that fails `when` is skipped
1434
+ // (no dispatch, excluded from the result). The concrete Node_* returns ONE RawElem_.
1290
1435
  lines.push(`${indent}for mk_${s} := range over_${s} {`);
1291
1436
  lines.push(`${indent}\t${elemLocal(s)} := over_${s}[mk_${s}]`);
1437
+ for (const l of emitGuard(indent + "\t", elemLocal(s)))
1438
+ lines.push(l);
1292
1439
  for (const l of buildPorts(indent + "\t", elemLocal(s)))
1293
1440
  lines.push(l);
1294
1441
  // per-element dispatch (ONE physical request per element). run_behavior passes the over element
@@ -1317,6 +1464,76 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1317
1464
  function elemLocal(s) {
1318
1465
  return `oel_${s}`;
1319
1466
  }
1467
+ /** #108: resolve an INPUT port head for a native-expr ref (cond `if` / map `when` / branches). A scalar
1468
+ * port → `in.<Field>` of the scalar type; an array port WITH elemType → `in.<Field>` of `[]ElemT` (so
1469
+ * `len(in.<Field>)` works); anything else → null (fail-closed). */
1470
+ function goInputHeadRef(head, comp, plan) {
1471
+ const schema = comp.inputPorts?.[head];
1472
+ if (schema === undefined)
1473
+ return null;
1474
+ const sc = inputScalarKind(schema);
1475
+ if (sc !== undefined) {
1476
+ const scalarRef = { kind: "scalar", scalar: sc === "int64" ? "int" : sc === "float64" ? "float" : sc };
1477
+ return { expr: `in.${goFieldName(head)}`, ref: scalarRef };
1478
+ }
1479
+ const et = schema.elemType;
1480
+ if ((schema.type === "array" || schema.type === "arr") && et !== undefined) {
1481
+ return { expr: `in.${goFieldName(head)}`, ref: { kind: "arr", elem: deriveTypeRef(et, plan) } };
1482
+ }
1483
+ return null;
1484
+ }
1485
+ /**
1486
+ * emitCondArm — the struct-native exec of a covered cond node (#108). Mirrors run_behavior's cond
1487
+ * lower (behavior.ts:425-429): compile `if` to a native bool, materialize the TAKEN branch to the
1488
+ * node's outType (only the taken branch evaluated — short-circuit), assign the node's typed cell and
1489
+ * set produced. NO handler dispatch (cond is a pure Expression). The `if`/`then`/`else` expressions
1490
+ * compile via the runtime-free NativeExprCompiler (byte-equal to `evaluate`); an uncovered op / opt /
1491
+ * refOpt / type-mismatch fails closed at emit.
1492
+ */
1493
+ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut) {
1494
+ const c = node.cond;
1495
+ const s = sanitize(node.id);
1496
+ const nodeRef = typedNodes.get(node.id);
1497
+ const priorHere = (head) => priorNodeAt(head, atPos);
1498
+ const resolveHead = (head) => {
1499
+ if (priorHere(head))
1500
+ return { expr: typedLocal(head), ref: typedNodes.get(head) };
1501
+ return goInputHeadRef(head, comp, plan);
1502
+ };
1503
+ const be = makeGoExprBackend(zeroOut, resolveHead, plan);
1504
+ const compiler = new NativeExprCompiler(be);
1505
+ const resolveDecl = (name) => findDecl(plan, name).fields;
1506
+ const cond = compiler.compileBool(c.if);
1507
+ const thenC = compiler.compileTo(c.then, nodeRef, resolveDecl);
1508
+ const elseC = compiler.compileTo(c.else, nodeRef, resolveDecl);
1509
+ const lines = [];
1510
+ lines.push(`\t// ── cond '${node.id}'${c.parent ? " (parent:" + c.parent + ")" : ""} — pure Expression (no handler) ──`);
1511
+ let indent = "\t";
1512
+ const closers = [];
1513
+ if (c.parent !== undefined && priorNodeAt(c.parent, atPos)) {
1514
+ lines.push(`${indent}if produced_${sanitize(c.parent)} {`);
1515
+ closers.unshift(`${indent}}`);
1516
+ indent += "\t";
1517
+ }
1518
+ // the `if` condition (its fallible temps hoisted before the branch — the condition is always evaluated).
1519
+ for (const st of cond.stmts)
1520
+ lines.push(`${indent}${st}`);
1521
+ lines.push(`${indent}if ${cond.expr} {`);
1522
+ // TAKEN = then: only this branch's stmts run (short-circuit ≡ evaluate).
1523
+ for (const st of thenC.stmts)
1524
+ lines.push(`${indent}\t${st}`);
1525
+ lines.push(`${indent}\t${typedLocal(node.id)} = ${thenC.expr}`);
1526
+ lines.push(`${indent}} else {`);
1527
+ for (const st of elseC.stmts)
1528
+ lines.push(`${indent}\t${st}`);
1529
+ lines.push(`${indent}\t${typedLocal(node.id)} = ${elseC.expr}`);
1530
+ lines.push(`${indent}}`);
1531
+ lines.push(`${indent}produced_${s} = true`);
1532
+ void nodeRef;
1533
+ for (const cl of closers)
1534
+ lines.push(cl);
1535
+ return lines.join("\n");
1536
+ }
1320
1537
  /**
1321
1538
  * emitProducedAwareValue — the #86 part 1 produced-aware output lowering (go). It mirrors the
1322
1539
  * boxed-typed emitTypedValue, but a `ref`/`refOpt` whose HEAD is a skippable relationSingle child is
@@ -1433,9 +1650,190 @@ function goZero(ref) {
1433
1650
  // bc#90 / runtime-free: the nv* helper set (nvVE/nvBindVE/nvFieldVE/nvCatVE/nvArrVE) is REMOVED —
1434
1651
  // those were dslcontracts.Value-based; the covered plane now lowers every port to a native Go value
1435
1652
  // (emitPortInit) so no VE helper is emitted, and the module imports ZERO bc runtime.
1653
+ // ── native expression compiler backend (#108) — cond `if` / map `when` / cond branches ──────────
1654
+ //
1655
+ // The runtime-free native expression compiler (native-expr.ts) mirrors runBehavior's cond/guard
1656
+ // Expression evaluation with BYTE-EQUAL semantics. The value-dependent runtime failures (INT_OVERFLOW
1657
+ // / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS / INVALID_LITERAL for a huge len) are emitted as calls to a
1658
+ // FIXED set of RUNTIME-FREE helper funcs baked into the module (bcExpr* below) — the covered module
1659
+ // still imports ZERO bc runtime. A fallible helper returns (T, *BehaviorError); the runner early-returns
1660
+ // the failure (its zero output value is `zeroOut`).
1661
+ let goExprTempSeq = 0;
1662
+ /** module-level accumulator: set true when the native-expr compiler emits a fallible helper call
1663
+ * (so emit() includes the helper library + imports `math`). Reset at the start of each emit(). */
1664
+ const goExprUsed = { helpers: false };
1665
+ /** GoNativeExprBackend — renders native-expr for Go. `zeroOut` is the runner's error-return zero
1666
+ * value; `resolveHead` maps a ref head (input port / input-array $as elem / prior node) to a typed
1667
+ * base expr. Sets goExprUsed.helpers when a fallible helper is emitted (so emit() bakes the helper lib). */
1668
+ function makeGoExprBackend(zeroOut, resolveHead, plan) {
1669
+ const used = goExprUsed;
1670
+ return {
1671
+ lang: "go",
1672
+ freshTemp: () => `ex_${goExprTempSeq++}`,
1673
+ renderType: (ref) => renderTypeRef(ref),
1674
+ scalarType: (s) => goScalar(s),
1675
+ strLit: (s) => JSON.stringify(s),
1676
+ boolLit: (b) => (b ? "true" : "false"),
1677
+ intLit: (dec) => `int64(${dec})`,
1678
+ floatLit: (n) => `float64(${goFloatLitExpr(n)})`,
1679
+ fieldAccess: (baseExpr, baseRef, path) => goTypedInternals.typedFieldAccess(baseExpr, baseRef, path, plan),
1680
+ resolveHead,
1681
+ hoistFallible: (temp, _ty, call) => {
1682
+ used.helpers = true;
1683
+ // a unique err var per hoist (Go `:=` re-declares fine in the same block only for NEW LHS; use a
1684
+ // fresh err name to avoid "no new variables on left side of :=" when several hoists share a block).
1685
+ const errv = `err_${goExprTempSeq++}`;
1686
+ return [`${temp}, ${errv} := ${call}`, `if ${errv} != nil {`, `\treturn ${zeroOut}, ${errv}`, `}`];
1687
+ },
1688
+ helperCall: (helper, args) => {
1689
+ used.helpers = true;
1690
+ return `bcExpr_${helper}(${args.join(", ")})`;
1691
+ },
1692
+ binOp: (op, a, b) => `(${a} ${op} ${b})`,
1693
+ concat: (parts) => {
1694
+ // native string + concat (all parts are native strings) — no import needed.
1695
+ return `(${parts.join(" + ")})`;
1696
+ },
1697
+ notOp: (a) => `(!${a})`,
1698
+ structLit: (name, inits) => `${name}{${inits.map((i) => `${goFieldName(i.field)}: ${i.expr}`).join(", ")}}`,
1699
+ arrLit: (elemTy, items) => `[]${elemTy}{${items.join(", ")}}`,
1700
+ ternary: (cond, t, e, ty) => `func() ${ty} { if ${cond} { return ${t} }; return ${e} }()`,
1701
+ shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
1702
+ // and: temp=false; if left { <rStmts>; temp=rExpr }. or: temp=true; if !left { <rStmts>; temp=rExpr }.
1703
+ const gate = op === "and" ? left : `!(${left})`;
1704
+ const seed = op === "and" ? "false" : "true";
1705
+ return [
1706
+ `var ${temp} bool = ${seed}`,
1707
+ `if ${gate} {`,
1708
+ ...rStmts.map((s) => `\t${s}`),
1709
+ `\t${temp} = ${rExpr}`,
1710
+ `}`,
1711
+ ];
1712
+ },
1713
+ condGuard: (temp, ty, cond, tStmts, tExpr, eStmts, eExpr) => [
1714
+ `var ${temp} ${ty}`,
1715
+ `if ${cond} {`,
1716
+ ...tStmts.map((s) => `\t${s}`),
1717
+ `\t${temp} = ${tExpr}`,
1718
+ `} else {`,
1719
+ ...eStmts.map((s) => `\t${s}`),
1720
+ `\t${temp} = ${eExpr}`,
1721
+ `}`,
1722
+ ],
1723
+ };
1724
+ }
1725
+ /** Go float literal (finite; matches goFloatLit convention used elsewhere). */
1726
+ function goFloatLitExpr(n) {
1727
+ if (Number.isInteger(n))
1728
+ return `${n}.0`;
1729
+ return String(n);
1730
+ }
1731
+ /**
1732
+ * emitExprHelpers — the FIXED runtime-free native-expr helper library (Go). Emitted once per covered
1733
+ * module that uses any cond/guard/arith. Each helper is BYTE-EQUAL to the reference core in
1734
+ * expr-eval.ts (checkI64 / checkFinite / mod trunc / div widen / code-point compare / lenCore). All
1735
+ * failures are the LOCAL *BehaviorError with the SAME code strings as run_behavior.
1736
+ */
1737
+ function emitExprHelpers() {
1738
+ return `// ── native-expr runtime-free helper library (#108) — byte-equal to expr-eval.ts core ──
1739
+ const bcI64Max int64 = 9223372036854775807
1740
+ const bcI64Min int64 = -9223372036854775808
1741
+
1742
+ func bcExpr_addI64(a, b int64) (int64, *BehaviorError) {
1743
+ r := a + b
1744
+ // signed overflow detection (a+b): same-sign operands, result flips sign.
1745
+ if (a > 0 && b > 0 && r < 0) || (a < 0 && b < 0 && r > 0) {
1746
+ return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
1747
+ }
1748
+ return r, nil
1749
+ }
1750
+ func bcExpr_subI64(a, b int64) (int64, *BehaviorError) {
1751
+ r := a - b
1752
+ if (a >= 0 && b < 0 && r < 0) || (a < 0 && b > 0 && r >= 0) {
1753
+ return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
1754
+ }
1755
+ return r, nil
1756
+ }
1757
+ func bcExpr_mulI64(a, b int64) (int64, *BehaviorError) {
1758
+ if a == 0 || b == 0 {
1759
+ return 0, nil
1760
+ }
1761
+ r := a * b
1762
+ if r/b != a || (a == bcI64Min && b == -1) {
1763
+ return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
1764
+ }
1765
+ return r, nil
1766
+ }
1767
+ func bcExpr_negI64(a int64) (int64, *BehaviorError) {
1768
+ if a == bcI64Min {
1769
+ return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
1770
+ }
1771
+ return -a, nil
1772
+ }
1773
+ func bcExpr_checkFinite(v float64) (float64, *BehaviorError) {
1774
+ if v != v || v > 1.7976931348623157e308 || v < -1.7976931348623157e308 {
1775
+ return 0, &BehaviorError{Code: "NAN_OR_INF", Message: "non-finite float"}
1776
+ }
1777
+ return v, nil
1778
+ }
1779
+ func bcExpr_addF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a + b) }
1780
+ func bcExpr_subF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a - b) }
1781
+ func bcExpr_mulF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a * b) }
1782
+ func bcExpr_negF(a float64) (float64, *BehaviorError) { return bcExpr_checkFinite(-a) }
1783
+ func bcExpr_modF(a, b float64) (float64, *BehaviorError) { m := math.Mod(a, b); return bcExpr_checkFinite(m) }
1784
+ func bcExpr_modI64(a, b int64) (int64, *BehaviorError) {
1785
+ if b == 0 {
1786
+ return 0, &BehaviorError{Code: "MOD_ZERO", Message: "int mod by zero"}
1787
+ }
1788
+ // Go % is truncated (sign of dividend) — matches modCore.
1789
+ r := a % b
1790
+ // overflow only at MinInt64 % -1 → 0 in Go (no overflow); still checkI64-safe.
1791
+ return r, nil
1792
+ }
1793
+ func bcExpr_widenI64F(a int64) (float64, *BehaviorError) {
1794
+ // div widening exact range is ±2^53 (WIDEN_EXACT). Outside → PRECISION_LOSS.
1795
+ if a > 9007199254740992 || a < -9007199254740992 {
1796
+ return 0, &BehaviorError{Code: "PRECISION_LOSS", Message: "int exceeds exact float range (±2^53)"}
1797
+ }
1798
+ return float64(a), nil
1799
+ }
1800
+ func bcExpr_divF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a / b) }
1801
+ func bcExpr_lenArr[T any](a []T) (int64, *BehaviorError) {
1802
+ return int64(len(a)), nil
1803
+ }
1804
+ // code-point (UTF-8 rune) order comparison — matches cmpCodePoints (NOT UTF-16 / byte compare, which
1805
+ // Go's native string < already is for valid UTF-8, but we compare rune-by-rune to match exactly).
1806
+ func bcExpr_cmpCP(a, b string) int {
1807
+ ra := []rune(a)
1808
+ rb := []rune(b)
1809
+ n := len(ra)
1810
+ if len(rb) < n {
1811
+ n = len(rb)
1812
+ }
1813
+ for i := 0; i < n; i++ {
1814
+ if ra[i] != rb[i] {
1815
+ if ra[i] < rb[i] {
1816
+ return -1
1817
+ }
1818
+ return 1
1819
+ }
1820
+ }
1821
+ if len(ra) == len(rb) {
1822
+ return 0
1823
+ }
1824
+ if len(ra) < len(rb) {
1825
+ return -1
1826
+ }
1827
+ return 1
1828
+ }
1829
+ func bcExpr_strLt(a, b string) bool { return bcExpr_cmpCP(a, b) < 0 }
1830
+ func bcExpr_strLe(a, b string) bool { return bcExpr_cmpCP(a, b) <= 0 }
1831
+ func bcExpr_strGt(a, b string) bool { return bcExpr_cmpCP(a, b) > 0 }
1832
+ func bcExpr_strGe(a, b string) bool { return bcExpr_cmpCP(a, b) >= 0 }`;
1833
+ }
1436
1834
  // ── BindNativeRaw dispatch surface ──────────────────────────────────────────────────────
1437
1835
  /** 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
1836
+ * (RunNativeRawStruct_<comp>) ARE the exposed API. The consumer calls the struct runner directly
1439
1837
  * (it returns the typed model — NO Value serialization on the read hot path, matching how graphddb
1440
1838
  * reads: a model, never a boxed Value). There is deliberately NO Value dispatch map — that would
1441
1839
  * require a Value serializer (the boxing residue bc#77 eliminates). A canonical Value observation
@@ -1443,7 +1841,7 @@ function goZero(ref) {
1443
1841
  * which lives OUTSIDE this module so the covered module stays literal whole-file zero. */
1444
1842
  function emitStructSurface(native) {
1445
1843
  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
1844
+ // order). Each is driven via RunNativeRawStruct_<comp>(handlers, input) -> (T, error): a STRUCT
1447
1845
  // return; the consumer keeps the model native (no Value serialization on the read hot path — the
1448
1846
  // boxing residue bc#77 removes). See INTEGRATION.md §6.
1449
1847
  var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name)).join(", ")}}`;
@@ -1453,11 +1851,13 @@ var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name
1453
1851
  * mismatch Sprintf, `strings` for a concat builder, `sync` for a parallel stage). ExpectedSpecVersions
1454
1852
  * stays as a local `var` for provenance, but the old `dslcontracts.SpecVersions` skew `init()` is GONE
1455
1853
  * (change #5 — there is no runtime to skew against once the module is runtime-free). */
1456
- function emitMinimalHeader(ctx, needStrings, needSync, needFmt) {
1854
+ function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath) {
1457
1855
  const sv = ctx.specVersions;
1458
1856
  const imports = [];
1459
1857
  if (needFmt)
1460
1858
  imports.push(`\t"fmt"`);
1859
+ if (needMath)
1860
+ imports.push(`\t"math"`);
1461
1861
  if (needStrings)
1462
1862
  imports.push(`\t"strings"`);
1463
1863
  if (needSync)
@@ -1484,6 +1884,9 @@ function emit(ctx) {
1484
1884
  const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
1485
1885
  const plan = buildTypePlan(ctx.ir);
1486
1886
  const decls = emitTypeDecls(plan);
1887
+ // #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler when it emits
1888
+ // a fallible checked-arith helper call — determines whether the helper library + `math` import ship).
1889
+ goExprUsed.helpers = false;
1487
1890
  // Emit the runners first so the nv* helper usage flags are known.
1488
1891
  const flags = { bind: false, field: false, cat: false, arr: false };
1489
1892
  const structs = [];
@@ -1493,18 +1896,23 @@ function emit(ctx) {
1493
1896
  const elemCopiers = [];
1494
1897
  const runnerCodes = [];
1495
1898
  for (const c of native) {
1496
- inStructs.push(emitInStruct(c));
1899
+ inStructs.push(emitInStruct(c, plan));
1497
1900
  const typedNodes = new Map();
1901
+ // build the full typed-node map FIRST so a map's over-element resolution can see every node.
1902
+ for (const n of c.body)
1903
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1498
1904
  for (const n of c.body) {
1499
1905
  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));
1906
+ // #86 pt2 / #108: a map node emits an element ports struct + a batch struct. The element ports
1907
+ // may read the `$as` binding resolved via the over-element type (prior node / input array).
1908
+ structs.push(emitMapPortsStructs(c, n, typedNodes, plan));
1909
+ }
1910
+ else if ("cond" in n) {
1911
+ // #108: a cond node has NO ports struct (it is a pure Expression — no handler dispatch).
1503
1912
  }
1504
1913
  else {
1505
1914
  structs.push(emitPortsStruct(c, n));
1506
1915
  }
1507
- typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1508
1916
  }
1509
1917
  // CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
1510
1918
  rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
@@ -1539,7 +1947,10 @@ function emit(ctx) {
1539
1947
  `box uncovered shapes. Implement native coverage for this shape, fix the spec if the shape is ` +
1540
1948
  `genuinely runtime-dynamic, or generate this component on the --ir dynamic interpreter surface.`);
1541
1949
  }
1542
- const header = emitMinimalHeader(ctx, false, needSync, needFmt);
1950
+ // #108: `math` is imported iff the native-expr compiler emitted a fallible helper (bcExpr_modF uses
1951
+ // math.Mod). The helper library itself is baked into the module only when used (goExprUsed.helpers).
1952
+ const needMath = goExprUsed.helpers;
1953
+ const header = emitMinimalHeader(ctx, false, needSync, needFmt, needMath);
1543
1954
  const banner = `// COMBINED READ layer (bc#90 — the RUNTIME-FREE read de-box; STRUCT-ONLY, FULLY-NATIVE surface). A
1544
1955
  // covered read is a strictly-sequential typed componentRef chain (point reads + relationKind:single
1545
1956
  // children), emitted ONLY here: each node builds a native ports struct by direct field assignment (every
@@ -1551,7 +1962,7 @@ function emit(ctx) {
1551
1962
  // *BehaviorError (same codes, byte-equal to run_behavior). Exec is INLINE sequential (no plan driver) so
1552
1963
  // a relation child reads the parent's REAL struct result via direct field access and the child-present
1553
1964
  // 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
1965
+ // (fixes #323/#74). The exposed API is the STRUCT-returning RunNativeRawStruct_<comp> (the consumer
1555
1966
  // keeps the model native — no serialization on the read hot path). This module is FULLY NATIVE: a naive
1556
1967
  // grep for boxing primitives OR for the bc-runtime package finds nothing, by design (bc#90/runtime-free).`;
1557
1968
  const sections = [
@@ -1559,12 +1970,19 @@ function emit(ctx) {
1559
1970
  `// 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
1971
  decls ? `// Typed struct declarations (outType-derived; hash-dedup — shared type plan).\n${decls}` : undefined,
1561
1972
  `// 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")}`,
1973
+ rowStructs.some((r) => r.trim().length > 0)
1974
+ ? `// 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")}`
1975
+ : undefined,
1563
1976
  `// Per-component CONCRETE handler interfaces (one typed Node_* method per covered node).\n${handlerIfaces.join("\n\n")}`,
1564
1977
  elemCopiers.length
1565
1978
  ? `// Augmented map-element copiers (over element fields + the concrete into row — pure assignment).\n${elemCopiers.join("\n\n")}`
1566
1979
  : undefined,
1567
- `// Native ports structs (one per componentRef node; typed per the static port type).\n${structs.join("\n\n")}`,
1980
+ structs.some((r) => r.trim().length > 0)
1981
+ ? `// Native ports structs (one per componentRef node; typed per the static port type).\n${structs.filter((r) => r.trim().length > 0).join("\n\n")}`
1982
+ : undefined,
1983
+ goExprUsed.helpers
1984
+ ? `// #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()}`
1985
+ : undefined,
1568
1986
  `// Combined read runners (STRUCT-returning — the fully de-plumbed path).\n${runnerCodes.join("\n\n")}`,
1569
1987
  structSurface,
1570
1988
  ].filter((s) => !!s);
@@ -1595,12 +2013,12 @@ export function goTypedNativeObserve(ir, runtimeImport) {
1595
2013
  // decode the generic *Obj input into the CONCRETE In_<comp> struct (test glue — ONE decode at the
1596
2014
  // top, OUTSIDE the covered hot path; a REQUIRED declared input is present so an absent key is a
1597
2015
  // 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}`;
2016
+ 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
2017
  })
1600
2018
  .join("\n");
1601
2019
  // input decoders (TEST glue): generic *Obj -> concrete In_<comp>. Scalar fields read the typed value
1602
2020
  // (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");
2021
+ const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
1604
2022
  // The observe companion drives EVERY covered component, so its handler type param H must satisfy
1605
2023
  // EVERY per-component concrete Handler_<comp> interface — a combined constraint embeds them all.
1606
2024
  const combinedConstraint = `allNativeRawHandlers`;
@@ -1621,6 +2039,8 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
1621
2039
  for (const n of c.body)
1622
2040
  typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1623
2041
  for (const n of c.body) {
2042
+ if ("cond" in n)
2043
+ continue; // #108: cond has no handler method (pure Expression) — no scripted impl.
1624
2044
  const ref = typedNodes.get(n.id);
1625
2045
  if ("map" in n) {
1626
2046
  const m = n.map;
@@ -1788,7 +2208,7 @@ ${constraintDecl}
1788
2208
  ${methodImpls.join("\n\n")}
1789
2209
 
1790
2210
  // 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
2211
+ // GENERIC over the CONCRETE combined handler type H — so each enclosed RunNativeRawStruct_* call is
1792
2212
  // instantiated at the concrete handler type (direct, devirtualized per-node method calls), not at an
1793
2213
  // interface. Instantiate with your concrete handler type to preserve the covered path's concreteness.
1794
2214
  func ObserveNativeRaw[H ${combinedConstraint}](name string, handlers H, input *${PKG}.Obj) (${PKG}.Value, error) {