behavior-contracts 0.4.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 (111) hide show
  1. package/README.md +78 -29
  2. package/dist/behavior.d.ts +7 -0
  3. package/dist/behavior.d.ts.map +1 -1
  4. package/dist/behavior.js.map +1 -1
  5. package/dist/builder.d.ts.map +1 -1
  6. package/dist/builder.js +10 -3
  7. package/dist/builder.js.map +1 -1
  8. package/dist/generator/async-plan.d.ts +78 -0
  9. package/dist/generator/async-plan.d.ts.map +1 -0
  10. package/dist/generator/async-plan.js +116 -0
  11. package/dist/generator/async-plan.js.map +1 -0
  12. package/dist/generator/core.d.ts +35 -0
  13. package/dist/generator/core.d.ts.map +1 -1
  14. package/dist/generator/core.js +1 -0
  15. package/dist/generator/core.js.map +1 -1
  16. package/dist/generator/{emit-straightline-typed-go.d.ts → emit-shared-go-typed.d.ts} +28 -17
  17. package/dist/generator/emit-shared-go-typed.d.ts.map +1 -0
  18. package/dist/generator/emit-shared-go-typed.js +572 -0
  19. package/dist/generator/emit-shared-go-typed.js.map +1 -0
  20. package/dist/generator/emit-shared-go.d.ts +29 -0
  21. package/dist/generator/emit-shared-go.d.ts.map +1 -0
  22. package/dist/generator/{emit-straightline-go.js → emit-shared-go.js} +17 -31
  23. package/dist/generator/emit-shared-go.js.map +1 -0
  24. package/dist/generator/{emit-straightline-typed-rust.d.ts → emit-shared-rust-typed.d.ts} +27 -17
  25. package/dist/generator/emit-shared-rust-typed.d.ts.map +1 -0
  26. package/dist/generator/emit-shared-rust-typed.js +576 -0
  27. package/dist/generator/emit-shared-rust-typed.js.map +1 -0
  28. package/dist/generator/{emit-straightline-rust.d.ts → emit-shared-rust.d.ts} +1 -36
  29. package/dist/generator/emit-shared-rust.d.ts.map +1 -0
  30. package/dist/generator/{emit-straightline-rust.js → emit-shared-rust.js} +7 -28
  31. package/dist/generator/emit-shared-rust.js.map +1 -0
  32. package/dist/generator/emit-shared-typescript.d.ts +12 -0
  33. package/dist/generator/emit-shared-typescript.d.ts.map +1 -0
  34. package/dist/generator/{emit-straightline-typescript.js → emit-shared-typescript.js} +9 -30
  35. package/dist/generator/emit-shared-typescript.js.map +1 -0
  36. package/dist/generator/emit-straightline-typed-typescript.d.ts +1 -1
  37. package/dist/generator/emit-straightline-typed-typescript.d.ts.map +1 -1
  38. package/dist/generator/emit-straightline-typed-typescript.js +1 -1
  39. package/dist/generator/emit-straightline-typed-typescript.js.map +1 -1
  40. package/dist/generator/emit-typed-native-go.d.ts +63 -0
  41. package/dist/generator/emit-typed-native-go.d.ts.map +1 -0
  42. package/dist/generator/emit-typed-native-go.js +2260 -0
  43. package/dist/generator/emit-typed-native-go.js.map +1 -0
  44. package/dist/generator/emit-typed-native-rust.d.ts +53 -0
  45. package/dist/generator/emit-typed-native-rust.d.ts.map +1 -0
  46. package/dist/generator/emit-typed-native-rust.js +2123 -0
  47. package/dist/generator/emit-typed-native-rust.js.map +1 -0
  48. package/dist/generator/index.d.ts +4 -12
  49. package/dist/generator/index.d.ts.map +1 -1
  50. package/dist/generator/index.js +16 -52
  51. package/dist/generator/index.js.map +1 -1
  52. package/dist/generator/native-expr.d.ts +120 -0
  53. package/dist/generator/native-expr.d.ts.map +1 -0
  54. package/dist/generator/native-expr.js +456 -0
  55. package/dist/generator/native-expr.js.map +1 -0
  56. package/dist/generator/straightline.d.ts +46 -0
  57. package/dist/generator/straightline.d.ts.map +1 -1
  58. package/dist/generator/straightline.js +65 -0
  59. package/dist/generator/straightline.js.map +1 -1
  60. package/dist/generator/typed.d.ts.map +1 -1
  61. package/dist/generator/typed.js +8 -0
  62. package/dist/generator/typed.js.map +1 -1
  63. package/dist/guard.js +14 -0
  64. package/dist/guard.js.map +1 -1
  65. package/dist/index.d.ts +2 -2
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +1 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/plan.d.ts +6 -2
  70. package/dist/plan.d.ts.map +1 -1
  71. package/dist/plan.js +4 -0
  72. package/dist/plan.js.map +1 -1
  73. package/package.json +1 -1
  74. package/dist/generator/emit-go.d.ts +0 -19
  75. package/dist/generator/emit-go.d.ts.map +0 -1
  76. package/dist/generator/emit-go.js +0 -79
  77. package/dist/generator/emit-go.js.map +0 -1
  78. package/dist/generator/emit-raw-abi-go.d.ts +0 -45
  79. package/dist/generator/emit-raw-abi-go.d.ts.map +0 -1
  80. package/dist/generator/emit-raw-abi-go.js +0 -345
  81. package/dist/generator/emit-raw-abi-go.js.map +0 -1
  82. package/dist/generator/emit-raw-abi-rust.d.ts +0 -45
  83. package/dist/generator/emit-raw-abi-rust.d.ts.map +0 -1
  84. package/dist/generator/emit-raw-abi-rust.js +0 -380
  85. package/dist/generator/emit-raw-abi-rust.js.map +0 -1
  86. package/dist/generator/emit-rust.d.ts +0 -25
  87. package/dist/generator/emit-rust.d.ts.map +0 -1
  88. package/dist/generator/emit-rust.js +0 -125
  89. package/dist/generator/emit-rust.js.map +0 -1
  90. package/dist/generator/emit-straightline-go.d.ts +0 -58
  91. package/dist/generator/emit-straightline-go.d.ts.map +0 -1
  92. package/dist/generator/emit-straightline-go.js.map +0 -1
  93. package/dist/generator/emit-straightline-php.d.ts +0 -46
  94. package/dist/generator/emit-straightline-php.d.ts.map +0 -1
  95. package/dist/generator/emit-straightline-php.js +0 -941
  96. package/dist/generator/emit-straightline-php.js.map +0 -1
  97. package/dist/generator/emit-straightline-python.d.ts +0 -36
  98. package/dist/generator/emit-straightline-python.d.ts.map +0 -1
  99. package/dist/generator/emit-straightline-python.js +0 -855
  100. package/dist/generator/emit-straightline-python.js.map +0 -1
  101. package/dist/generator/emit-straightline-rust.d.ts.map +0 -1
  102. package/dist/generator/emit-straightline-rust.js.map +0 -1
  103. package/dist/generator/emit-straightline-typed-go.d.ts.map +0 -1
  104. package/dist/generator/emit-straightline-typed-go.js +0 -1111
  105. package/dist/generator/emit-straightline-typed-go.js.map +0 -1
  106. package/dist/generator/emit-straightline-typed-rust.d.ts.map +0 -1
  107. package/dist/generator/emit-straightline-typed-rust.js +0 -1153
  108. package/dist/generator/emit-straightline-typed-rust.js.map +0 -1
  109. package/dist/generator/emit-straightline-typescript.d.ts +0 -47
  110. package/dist/generator/emit-straightline-typescript.d.ts.map +0 -1
  111. package/dist/generator/emit-straightline-typescript.js.map +0 -1
@@ -0,0 +1,2260 @@
1
+ import { GeneratorFailure } from "./core.js";
2
+ import { goStraightlineInternals } from "./emit-shared-go.js";
3
+ import { goTypedInternals } from "./emit-shared-go-typed.js";
4
+ import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
5
+ import { concurrencyPlan, execPlan, analyzeSequentialOps, classifyExpr, buildComponentPlan, } from "./straightline.js";
6
+ import { NativeExprCompiler } from "./native-expr.js";
7
+ const PKG = "dslcontracts";
8
+ /** goErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
9
+ * plane (runtime-free). The covered module declares `type BehaviorError struct { Code, Message string }`
10
+ * (see emitBehaviorErrorType); a `*BehaviorError` satisfies the runner's `error` return. This REPLACES
11
+ * the old `dslcontracts.NewBehaviorFailure`/`NewPlanFailure` (the last error-plane runtime coupling) —
12
+ * the SAME codes are preserved verbatim so the observed failure is byte-equal to run_behavior. */
13
+ function goErr(code, msgExpr) {
14
+ return `&BehaviorError{Code: ${JSON.stringify(code)}, Message: ${msgExpr}}`;
15
+ }
16
+ /** emitBehaviorErrorType — the LOCAL concrete error type baked into the covered module so a runner can
17
+ * fail closed with ZERO bc-runtime import. `*BehaviorError` satisfies `error`. */
18
+ function emitBehaviorErrorType() {
19
+ return `// BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
20
+ // covered runner returns *BehaviorError (which satisfies error) instead of a bc-runtime failure, so
21
+ // the fully-covered module imports ZERO bc runtime. Codes match run_behavior verbatim (byte-equal).
22
+ type BehaviorError struct {
23
+ Code string
24
+ Message string
25
+ }
26
+
27
+ func (e *BehaviorError) Error() string { return e.Code + ": " + e.Message }
28
+
29
+ // FailureCode exposes the stable failure code (byte-equal to run_behavior) WITHOUT a bc-runtime type:
30
+ // a caller that wants the code type-asserts the small local interface { FailureCode() string } rather
31
+ // than a bc failure type — keeping this module runtime-free while a harness can still read the code.
32
+ func (e *BehaviorError) FailureCode() string { return e.Code }`;
33
+ }
34
+ const { sanitize, emitExpr } = goStraightlineInternals;
35
+ const { goFieldName, renderTypeRef, goScalar, serializeTyped, emitTypeDecls, emitSerializers, emitTypedValue, emitMustHelpers, } = goTypedInternals;
36
+ /** typed node-result local — MUST match goTypedInternals.typedLocal (`t_<id>`) so the reused
37
+ * emitTypedValue (output assembly via struct field access) reads the same locals. */
38
+ function typedLocal(nodeId) {
39
+ return `t_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
40
+ }
41
+ function goPortFieldName(wire) {
42
+ const safe = wire.replace(/[^A-Za-z0-9_]/g, "_");
43
+ const head = safe.charAt(0);
44
+ return /[a-z]/.test(head) ? head.toUpperCase() + safe.slice(1) : "F_" + safe;
45
+ }
46
+ function portsStructName(compName, nodeId) {
47
+ // EXPORTED so a consumer (and the test bench, both in a DIFFERENT Go package) can name the ports
48
+ // type to implement the per-component Handler_<comp> interface's concrete Node_* method signatures.
49
+ return `PortsNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
50
+ }
51
+ function inferPortType(node, inputPorts, asBinding) {
52
+ if (staticArrElems(node) !== null)
53
+ return "Value";
54
+ const e = classifyExpr(node);
55
+ switch (e.kind) {
56
+ case "str":
57
+ case "concat":
58
+ return "string";
59
+ case "bool":
60
+ return "bool";
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
+ }
74
+ if (e.path.length === 1) {
75
+ const s = inputPorts[e.path[0]];
76
+ if (s) {
77
+ if (s.type === "string")
78
+ return "string";
79
+ if (s.type === "int")
80
+ return "int64";
81
+ if (s.type === "float")
82
+ return "float64";
83
+ if (s.type === "bool")
84
+ return "bool";
85
+ }
86
+ }
87
+ return "Value";
88
+ }
89
+ default:
90
+ return "Value";
91
+ }
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
+ }
97
+ /** A static string-array port (every element a static string literal) — the graphddb `projection`
98
+ * shape. Returns the list of literal strings, or null when the node is not that shape. */
99
+ function staticStringArrElems(node) {
100
+ const arr = staticArrElems(node);
101
+ if (arr === null)
102
+ return null;
103
+ const out = [];
104
+ for (const el of arr) {
105
+ const e = classifyExpr(el);
106
+ if (e.kind !== "str")
107
+ return null;
108
+ out.push(e.value);
109
+ }
110
+ return out;
111
+ }
112
+ /**
113
+ * portFieldGoType — the NATIVE Go type for a covered port field (bc#90 / runtime-free). The covered
114
+ * ports struct carries NO `dslcontracts.Value` field: every port lowers to a concrete Go type.
115
+ * - a static string-array (projection) → `[]string` (change #2);
116
+ * - a bare int/float number literal (e.g. Query `limit: 20`) → `int64`/`float64`;
117
+ * - a string/bool/scalar-input port → the scalar Go type (inferPortType).
118
+ * A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
119
+ * Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no
120
+ * `dslcontracts.Value` fallback on the native module). `where` names the emission site for the error.
121
+ */
122
+ function portFieldGoType(node, inputPorts, where = "port", asBinding) {
123
+ if (staticStringArrElems(node) !== null)
124
+ return "[]string";
125
+ const num = numLiteralGoExpr(node);
126
+ if (num !== null)
127
+ return Number.isInteger(node) ? "int64" : "float64";
128
+ const ft = inferPortType(node, inputPorts, asBinding);
129
+ if (ft === "Value") {
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.`);
131
+ }
132
+ return ft;
133
+ }
134
+ function staticArrElems(node) {
135
+ if (node === null || typeof node !== "object" || Array.isArray(node))
136
+ return null;
137
+ const keys = Object.keys(node);
138
+ if (keys.length !== 1 || keys[0] !== "arr")
139
+ return null;
140
+ const arg = node.arr;
141
+ return Array.isArray(arg) ? arg : null;
142
+ }
143
+ /**
144
+ * isNativeRawComponent — a component covered by the combined read emitter. Strictly sequential
145
+ * (no real concurrency), body is ALL componentRef (no map/cond), default 'fail'/'continue'
146
+ * policy, relationKind is single/connection or absent, and EVERY node carries outType + the
147
+ * component carries outputType. relationKind:single / bindField ARE the point — inline exec makes
148
+ * the child-present decision from the real parent result. Ports AND the output are evaluated via
149
+ * the primitive SSoT over a Value-space portScope (so a dynamic operator in a port or an
150
+ * obj-assembly output is fine — the SSoT handles fail-closed semantics; only the RESULT plane +
151
+ * PORT container go native). A covered SHAPE that lacks outType is a fail-closed error raised at
152
+ * emit (see assertTypedCoverage) — never a silent re-box.
153
+ */
154
+ /**
155
+ * numLiteralGoExpr — a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
156
+ * `classifyExpr` deliberately leaves a bare number `dynamic` (its checked int/float range rules are
157
+ * the evaluate SSoT, expr.go evalNumberLiteral), so a plain number never reaches the str/bool/null
158
+ * static branch. But a number literal IS statically resolvable to a native Go Value: the SAME
159
+ * range check the runtime applies is performed HERE at generation time (integral literal in the
160
+ * safe 2^53-1 window → int64; a fractional/exponent literal → the checked float64). Returns the Go
161
+ * expression for the literal's Value, or null when the node is not a bare number OR the integral
162
+ * literal is out of the safe window (that out-of-range case keeps the runtime INVALID_LITERAL
163
+ * semantics — the port is NOT covered natively, so the whole component falls through, never a
164
+ * silently-wrong inline). Mirrors coderuntime.evalNumberLiteral exactly.
165
+ */
166
+ const NUM_SAFE_INT = 9007199254740991; // 2^53 - 1 (expr.go safeInt)
167
+ function numLiteralGoExpr(node) {
168
+ if (typeof node !== "number" || !Number.isFinite(node))
169
+ return null;
170
+ if (Number.isInteger(node)) {
171
+ if (node < -NUM_SAFE_INT || node > NUM_SAFE_INT)
172
+ return null; // out of safe window → not covered (runtime INVALID_LITERAL)
173
+ return `${PKG}.Value(int64(${node}))`;
174
+ }
175
+ // fractional / exponent literal → checked float64 (finite, already guaranteed above).
176
+ return `${PKG}.Value(float64(${node}))`;
177
+ }
178
+ /** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
179
+ * resolvable to a native Go Value with no NewObj / no evaluate interpreter. */
180
+ function portIsStatic(node) {
181
+ const arr = staticArrElems(node);
182
+ if (arr !== null)
183
+ return arr.every(portIsStatic);
184
+ if (numLiteralGoExpr(node) !== null)
185
+ return true; // bare number literal (e.g. Query `limit`)
186
+ const e = classifyExpr(node);
187
+ switch (e.kind) {
188
+ case "str":
189
+ case "bool":
190
+ case "null":
191
+ case "ref":
192
+ return true;
193
+ case "concat":
194
+ return e.parts.every((p) => portIsStatic(reconstructG(p)));
195
+ default:
196
+ return false;
197
+ }
198
+ }
199
+ /** The output must lower to a typed struct/value with no Value-scope read: a ref whose head is a
200
+ * BODY NODE (typed), an obj/arr of native-lowerable outputs, or a concat of them. A ref to an
201
+ * input param or a non-concat operator is NOT native-lowerable here (falls through). */
202
+ function outputIsNativeLowerable(comp) {
203
+ const bodyIds = new Set(comp.body.map((n) => n.id));
204
+ const walk = (node) => {
205
+ const arr = staticArrElems(node);
206
+ if (arr !== null)
207
+ return arr.every(walk);
208
+ if (node !== null && typeof node === "object" && !Array.isArray(node)) {
209
+ const keys = Object.keys(node);
210
+ if (keys.length === 1 && keys[0] === "obj") {
211
+ const obj = node.obj;
212
+ return Object.keys(obj).every((k) => walk(obj[k]));
213
+ }
214
+ if (keys.length === 1 && keys[0] === "arr")
215
+ return true; // handled by staticArrElems above
216
+ }
217
+ const e = classifyExpr(node);
218
+ if (e.kind === "str" || e.kind === "bool" || e.kind === "null")
219
+ return true;
220
+ // ref OR refOpt to a body node (typed). refOpt is how a produced-aware output reads a skippable
221
+ // relationSingle child: the head node's produced flag gates it, yielding opt-nil when unproduced.
222
+ if (e.kind === "ref")
223
+ return bodyIds.has(e.path[0]); // must ref a body node (typed), not input
224
+ if (e.kind === "concat")
225
+ return e.parts.every((p) => walk(reconstructG(p)));
226
+ return false;
227
+ };
228
+ return walk(comp.output);
229
+ }
230
+ /** A native-raw component MUST be fully typed (outType on every node + outputType). */
231
+ function isFullyTyped(comp) {
232
+ if (comp.outputType === undefined)
233
+ return false;
234
+ for (const n of comp.body) {
235
+ if (n.outType === undefined)
236
+ return false;
237
+ }
238
+ return true;
239
+ }
240
+ function isNativeRaw(comp) {
241
+ return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
242
+ }
243
+ /**
244
+ * coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet). A BATCHED `map` node whose:
245
+ * - over is a static ref (to a prior node's typed collection field OR an input array),
246
+ * - `into` is set (zip-attach the batched handler result onto every over element),
247
+ * - every element port is static (literal / ref into the element binding / concat / static arr),
248
+ * - element outType is a NAMED struct (the augmented element type — over element + the into field),
249
+ * - no `when` guard (guard+into => heterogeneous elements, not one []T — stays boxed),
250
+ * - default 'fail' policy, relationKind single/connection or absent.
251
+ * This is the struct-native de-box of a typed `map...into` element collection: build the per-element
252
+ * native ports struct, dispatch the batched handler ONCE (RawValue out), and assemble each augmented
253
+ * element struct by copying the over element's typed fields + materializing the into field from the
254
+ * aligned batch raw — NO boxed Value on any plane.
255
+ */
256
+ function coveredMapNode(n, bodyIds, comp) {
257
+ if (n === null || typeof n !== "object" || !("map" in n))
258
+ return false;
259
+ const m = n.map;
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)
265
+ return false;
266
+ if (m.policy !== undefined && m.policy !== "fail")
267
+ return false;
268
+ if (m.relationKind !== undefined && m.relationKind !== "single" && m.relationKind !== "connection")
269
+ return false;
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).
273
+ const overE = classifyExpr(m.over);
274
+ if (overE.kind !== "ref")
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
+ }
285
+ const ports = m.ports;
286
+ for (const p of Object.keys(ports))
287
+ if (!portIsStatic(ports[p]))
288
+ return false;
289
+ return true;
290
+ }
291
+ /** A covered read SHAPE (typing-independent) — sequential componentRef reads + covered map...into,
292
+ * OR a real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point
293
+ * reads. A real-concurrency plan whose parallel members include a map / bindField-relation child is
294
+ * NOT covered here (falls through) — the static-parallel orchestration covers the componentRef fan-out
295
+ * (the GroupAccessRead shape); a parallel map/relation-child stage is a further shape (out of scope). */
296
+ function isSequentialComponentRefRead(comp) {
297
+ return coverageRejectReason(comp) === null;
298
+ }
299
+ /**
300
+ * coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89). Returns
301
+ * null when the component IS a covered native read; otherwise a SPECIFIC human-readable reason naming
302
+ * the exact disqualifier and the offending node id. The predicate logic is IDENTICAL to
303
+ * isSequentialComponentRefRead (which is defined as `coverageRejectReason(c) === null`) so the two can
304
+ * never disagree. This is the message the fail-closed dispatch surfaces per non-native component — the
305
+ * user's signal to add native coverage, fix a genuinely-dynamic spec, or use the --ir dynamic surface.
306
+ */
307
+ function coverageRejectReason(comp) {
308
+ const ep = execPlan(comp);
309
+ if (ep === null)
310
+ return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
311
+ // A parallel-stage member must be a plain componentRef point read (no map, no bindField) so the
312
+ // static parallel orchestration is a clean fan-out — the bounded-concurrency error-precedence
313
+ // protocol is proven for that shape. A map / bindField child inside a parallel stage falls through.
314
+ for (const [i] of ep.parallelStageOf) {
315
+ const n = comp.body[i];
316
+ if (!("component" in n))
317
+ return `node '${n.id}' is a non-componentRef member of a parallel stage (not native-covered)`;
318
+ if (n.bindField !== undefined)
319
+ return `node '${n.id}' is a bindField relation child inside a parallel stage (not native-covered)`;
320
+ }
321
+ const bodyIds = new Set(comp.body.map((b) => b.id));
322
+ for (const n of comp.body) {
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
+ }
332
+ if ("map" in n) {
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)`;
338
+ continue;
339
+ }
340
+ if (!("component" in n))
341
+ return `node '${n.id}' is not a componentRef, map, or cond node (not native-covered)`;
342
+ const cr = n;
343
+ // Only default 'fail' policy on covered reads: 'continue' (and any skip source, incl. bindField)
344
+ // makes a node UNPRODUCED at runtime, whose output ref must yield the unproduced rep — a
345
+ // produced-aware output lowering the struct-only path does not do yet (documented follow-up).
346
+ // Such components fall through to the boxed typed / straight-line path (correct, just not
347
+ // struct-de-plumbed). relationKind:single WITHOUT bindField (the #74/#323 core) IS covered.
348
+ if (cr.policy !== undefined && cr.policy !== "fail")
349
+ return `node '${n.id}' componentRef policy '${cr.policy}' (produced-aware output not yet native-covered)`;
350
+ // #86 part 1: a relationKind:single child WITH bindField (relationSingle) IS covered. When the
351
+ // bound parent field is null/absent the child is UNPRODUCED — the produced-aware output lowering
352
+ // (emitProducedAwareValue) then yields the unproduced rep (null / opt-nil) for a ref/refOpt into
353
+ // it, matching run_behavior's writeUnproduced. #93 shape 1: a bindField on a relationKind:connection
354
+ // child is ALSO covered — the connection's unproduced rep is the empty connection {items:[],cursor:null}
355
+ // (unproducedValue("connection")), which emitProducedAwareValue emits explicitly for a skipped
356
+ // connection node (a ref into it yields {items:[],cursor:null}, matching run_behavior).
357
+ if (cr.bindField !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
358
+ return `node '${n.id}' relationKind '${cr.relationKind ?? "point"}' not native-covered (bindField only supports single|connection)`;
359
+ if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
360
+ return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
361
+ for (const p of Object.keys(cr.ports))
362
+ if (portIsStatic(cr.ports[p]) === false)
363
+ return `node '${n.id}' port '${p}' is not statically resolvable`;
364
+ }
365
+ if (!outputIsNativeLowerable(comp))
366
+ return "output is not natively lowerable";
367
+ return null;
368
+ }
369
+ /** Fail closed if a covered read SHAPE lacks the typed annotations (no silent boxed re-box). */
370
+ function assertTypedCoverage(comp) {
371
+ if (!isSequentialComponentRefRead(comp))
372
+ return;
373
+ if (isFullyTyped(comp))
374
+ return;
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).`);
376
+ }
377
+ // ── native ports struct + PortReader ──────────────────────────────────────────────────
378
+ function emitPortsStruct(comp, node, asBinding) {
379
+ const structName = portsStructName(comp.name, node.id);
380
+ const portNames = Object.keys(node.ports);
381
+ if (portNames.length === 0) {
382
+ return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
383
+ }
384
+ const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding));
385
+ const fieldNames = portNames.map(goPortFieldName);
386
+ const fields = portNames
387
+ .map((p, i) => `\t${fieldNames[i]} ${fieldTypes[i]} // ${JSON.stringify(p)}`)
388
+ .join("\n");
389
+ // NO PortReader `Port(name)` method: the covered handler reads the TYPED struct fields (ports.F_PK)
390
+ // DIRECTLY, so the boxed-Value PortReader impl (the last per-port `dslcontracts.Value` wrap on the
391
+ // covered plane) is DEAD — dropping it makes the ports struct fully native (bc#90 / runtime-free).
392
+ return `// ${structName} — NATIVE ports for node '${node.id}' (${node.component}). Typed fields per the
393
+ // static port type; built by direct field assignment (no map alloc, no per-port boxing). The consumer
394
+ // reads the typed fields DIRECTLY (no PortReader / boxed-Value accessor) — the covered read is native.
395
+ type ${structName} struct {
396
+ ${fields}
397
+ }`;
398
+ }
399
+ // ── CONCRETE per-node row structs + row→outType copiers (bc: full Go concreteness) ──────────
400
+ //
401
+ // The covered path no longer crosses a generic `RawValue`/`RawRow` on the RESULT plane. Each
402
+ // covered node has a CONCRETE row struct `RawRow_<comp>_<node>` whose fields are the node's
403
+ // projected outType fields (typed, flattened) plus a per-node error signal (IsError/Err). The
404
+ // per-component `Handler_<comp>` interface has one method per node returning that concrete struct;
405
+ // the runner reads `row.<Field>` DIRECTLY (no `.(RawRow)`, no `.(scalar)`, no `RawValue`) and copies
406
+ // each field into the node's outType struct local. The `.(T)` type-assert seam that the generic
407
+ // RawValue de-box carried is GONE on the covered path (the handler produces the typed fields).
408
+ //
409
+ // A scalar/arr/opt node (outType not a named struct) has a single `Val <typed>` payload field. A
410
+ // named-struct node has one Go field per outType field (same names/types as the outType decl), so
411
+ // `RawRow_<comp>_<node>` is structurally the outType struct + {IsError, Err}. The copier is then a
412
+ // mechanical field-for-field assignment into the outType struct — NO dynamic value read anywhere.
413
+ /** Row_<comp>_<node> — the concrete per-node handler-result row struct name. (Deliberately NOT
414
+ * prefixed with the generic dynamic-accessor name — that accessor is what the concrete path removes.) */
415
+ function rawRowStructName(compName, nodeId) {
416
+ return `Row_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
417
+ }
418
+ /** RowElem_<comp>_<node> — the concrete per-ELEMENT row struct name for a covered map node (the
419
+ * element/into row the map handler yields per over element). */
420
+ function rawElemStructName(compName, nodeId) {
421
+ return `RowElem_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
422
+ }
423
+ /** Handler_<comp> — the per-component concrete handler interface name (one Node_* method per node). */
424
+ function handlerIfaceName(compName) {
425
+ return `Handler_${sanitize(compName)}`;
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
+ }
435
+ /** Node_<comp>_<nodeId> — the concrete per-node handler method name. */
436
+ function nodeMethodName(compName, nodeId) {
437
+ return `Node_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
438
+ }
439
+ /**
440
+ * emitRawRowStructs — emit the concrete `RawRow_<comp>_<node>` struct per covered node (and, for a
441
+ * covered map node, the per-element `RawElem_<comp>_<node>`). A named-struct outType flattens to one
442
+ * Go field per outType field; a scalar/arr/opt node carries a single `Val` payload. Every row carries
443
+ * the per-node error signal (IsError/Err) so the runner interprets policy without a boxed outcome.
444
+ */
445
+ function emitRawRowStructs(comp, typedNodes, plan) {
446
+ const parts = [];
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;
451
+ const ref = typedNodes.get(n.id);
452
+ if ("map" in n) {
453
+ // a covered map node: RawElem_<comp>_<node> is the HANDLER'S per-element return shape:
454
+ // - into: the `into` field's type (the fetched item merged onto every over element);
455
+ // - no-into (#93 shape 3): the element outType directly (connection-collect).
456
+ // The batched Node_* returns RawRow_<comp>_<node> (aligned []RawElem_ + error); a per-element
457
+ // dispatch Node_* returns RawElem_ directly (one physical call per element).
458
+ const arrRef = ref;
459
+ const elemRowRef = mapElemRowRef(n, arrRef, plan);
460
+ parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
461
+ parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
462
+ `type ${rawRowStructName(comp.name, n.id)} struct {\n\tIsError bool\n\tErr string\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
463
+ continue;
464
+ }
465
+ parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
466
+ }
467
+ return parts.join("\n\n");
468
+ }
469
+ /** Emit one concrete row struct: flatten a named outType to typed fields, else a single Val payload;
470
+ * always carry IsError/Err. */
471
+ function emitOneRowStruct(name, ref, plan) {
472
+ const lines = [];
473
+ lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType; + error signal).`);
474
+ lines.push(`type ${name} struct {`);
475
+ lines.push(`\tIsError bool`);
476
+ lines.push(`\tErr string`);
477
+ if (ref.kind === "named") {
478
+ const decl = findDecl(plan, ref.name);
479
+ for (const f of decl.fields) {
480
+ lines.push(`\t${goFieldName(f.name)} ${renderTypeRef(f.type)} // ${JSON.stringify(f.name)}`);
481
+ }
482
+ }
483
+ else {
484
+ lines.push(`\tVal ${renderTypeRef(ref)}`);
485
+ }
486
+ lines.push(`}`);
487
+ return lines.join("\n");
488
+ }
489
+ /** emitRowCopy — the statements copying a concrete row's typed fields into the node's outType local
490
+ * `target` (a value of Go type renderTypeRef(ref)). Named: field-for-field; scalar/arr/opt: `.Val`.
491
+ * `rowExpr` is the concrete RawRow_/RawElem_ value; `indent` tabs. No dynamic read — pure assignment. */
492
+ function emitRowCopy(target, rowExpr, ref, plan, indent) {
493
+ const t = "\t".repeat(indent);
494
+ if (ref.kind === "named") {
495
+ const decl = findDecl(plan, ref.name);
496
+ return decl.fields.map((f) => `${t}${target}.${goFieldName(f.name)} = ${rowExpr}.${goFieldName(f.name)}`).join("\n");
497
+ }
498
+ return `${t}${target} = ${rowExpr}.Val`;
499
+ }
500
+ /**
501
+ * boundGoType (change #3 — typed `bound`) — the CONCRETE Go type of a covered node's `bound` handler
502
+ * param (bc#90 / runtime-free). `bound` is the parent-produced key (the bindField value); it replaces
503
+ * the old boxed `dslcontracts.Value`. We represent PRODUCED-vs-UNPRODUCED with a POINTER: a non-nil
504
+ * `*T` means the parent value was produced/present (an empty string is a VALID produced value — a
505
+ * non-nil `*string` to ""); a nil pointer means UNPRODUCED/skip (exactly the old `!= nil` guard ≡
506
+ * run_behavior). The pointed-to type is the bindField's concrete Go type (a string key here). A node
507
+ * WITHOUT a bindField never receives a bound value — its param is a `*string` placeholder always
508
+ * passed `nil` (a typed nil, so the ABI stays uniform and carries NO dslcontracts.Value).
509
+ */
510
+ function boundGoType(node, comp, typedNodes, plan) {
511
+ if (node.bindField === undefined || node.parent === undefined)
512
+ return "*string";
513
+ const parentRef = typedNodes.get(node.parent);
514
+ if (parentRef === undefined)
515
+ return "*string";
516
+ let bfRef;
517
+ try {
518
+ bfRef = goTypedInternals.typedFieldAccess(typedLocal(node.parent), parentRef, [node.bindField], plan).ref;
519
+ }
520
+ catch {
521
+ return "*string";
522
+ }
523
+ // opt bindField: the field is already a *T — the bound pointed-to type is the inner. A required
524
+ // scalar bindField: the bound pointed-to type is that scalar's Go type.
525
+ if (bfRef.kind === "opt")
526
+ return `*${renderTypeRef(bfRef.inner)}`;
527
+ return `*${renderTypeRef(bfRef)}`;
528
+ }
529
+ /**
530
+ * emitHandlerInterface — the per-component `Handler_<comp>` interface: one concrete-typed `Node_*`
531
+ * method per covered node. Each componentRef node method takes the node's native ports struct + the
532
+ * typed `bound` pointer (change #3 — the produced-aware parent key; NO boxed dslcontracts.Value) and
533
+ * returns its concrete row struct + a resolved bool. A covered map node's method takes the batch
534
+ * ports struct (batched) or the element ports struct (per-element) and returns the batch / element
535
+ * row. This is the fully-concrete, runtime-free replacement for the generic ExecNativeRaw seam.
536
+ */
537
+ function emitHandlerInterface(comp, typedNodes, plan) {
538
+ const lines = [];
539
+ lines.push(`// ${handlerIfaceName(comp.name)} — the CONCRETE per-component handler seam: one typed method`);
540
+ lines.push(`// per covered node (native ports struct IN, concrete row struct OUT). No generic boxed-ports`);
541
+ lines.push(`// / boxed-value / dynamic accessor crosses the covered boundary — the consumer implements each`);
542
+ lines.push(`// Node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
543
+ lines.push(`type ${handlerIfaceName(comp.name)} interface {`);
544
+ for (const n of comp.body) {
545
+ if ("cond" in n)
546
+ continue; // #108: a cond node has no handler method (pure Expression).
547
+ const method = nodeMethodName(comp.name, n.id);
548
+ const bt = boundGoType(n, comp, typedNodes, plan);
549
+ if ("map" in n) {
550
+ const m = n.map;
551
+ const batched = m.batched === true;
552
+ const portsT = batched ? `${portsStructName(comp.name, n.id)}_batch` : portsStructName(comp.name, n.id);
553
+ const retT = batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
554
+ lines.push(`\t${method}(ports ${portsT}, bound ${bt}) (${retT}, bool)`);
555
+ continue;
556
+ }
557
+ lines.push(`\t${method}(ports ${portsStructName(comp.name, n.id)}, bound ${bt}) (${rawRowStructName(comp.name, n.id)}, bool)`);
558
+ }
559
+ lines.push(`}`);
560
+ return lines.join("\n");
561
+ }
562
+ /** mapElemMaterializerName — the augmented-element copier for a covered map...into node. */
563
+ function mapElemMaterializerName(compName, nodeId) {
564
+ return `materializeMapElemNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
565
+ }
566
+ /**
567
+ * emitMapPortsStructs — the per-element native ports struct + the batch struct for a covered map
568
+ * node (bc#90 / runtime-free). The element struct is the same shape as a componentRef ports struct
569
+ * (typed fields per the static port type). The batch struct carries `Items []<elemStruct>` — a NATIVE
570
+ * slice of the concrete per-element ports structs (NO `[]dslcontracts.Value`, NO PortReader accessor);
571
+ * the consumer reads `bp.Items[i].F_PK` directly.
572
+ */
573
+ function emitMapPortsStructs(comp, node, typedNodes, plan) {
574
+ const m = node.map;
575
+ const structName = portsStructName(comp.name, node.id);
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);
582
+ const batchStruct = `${structName}_batch`;
583
+ return `${elemStruct}
584
+
585
+ // ${batchStruct} — NATIVE batched ports for map '${node.id}': Items is a native slice of the concrete
586
+ // per-element ports structs (${structName}). The consumer reads Items[i].<Field> DIRECTLY — no boxed
587
+ // []Value, no PortReader accessor, no key-value object (bc#90 / runtime-free).
588
+ type ${batchStruct} struct {
589
+ Items []${structName}
590
+ }`;
591
+ }
592
+ /** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
593
+ * type (into) or the element outType directly (no-into). This is the type of RawElem_<comp>_<node>. */
594
+ function mapElemRowRef(node, arrRef, plan) {
595
+ const m = node.map;
596
+ if (m.into === undefined)
597
+ return arrRef.elem;
598
+ if (arrRef.elem.kind !== "named") {
599
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined map (#86 pt2): node '${node.id}' element outType must be a named struct (got ${JSON.stringify(arrRef.elem.kind)}) — the augmented element de-boxes into a struct. Regenerate as boxed typed/straight-line.`);
600
+ }
601
+ const decl = findDecl(plan, arrRef.elem.name);
602
+ const intoField = decl.fields.find((f) => f.name === m.into);
603
+ if (!intoField) {
604
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined map (#86 pt2): element struct '${arrRef.elem.name}' has no '${m.into}' (into) field.`);
605
+ }
606
+ return intoField.type;
607
+ }
608
+ /**
609
+ * emitMapElemMaterializers — for each covered map...into node, emit the AUGMENTED-element copier: copy
610
+ * the over element's typed fields + assign the `into` field from the concrete per-element handler row
611
+ * (RawElem_<comp>_<node>). Pure struct field assignment — NO dynamic value read (the handler already
612
+ * produced the typed into row).
613
+ */
614
+ function emitMapElemMaterializers(comp, typedNodes, plan) {
615
+ const parts = [];
616
+ for (const n of comp.body) {
617
+ if (!("map" in n))
618
+ continue;
619
+ const m = n.map;
620
+ // #93 shape 3: a map with NO `into` collects each element DIRECTLY from the per-element handler row
621
+ // (RawElem_ IS the element outType) — no over-element merge, so no augmented-element copier here.
622
+ if (m.into === undefined)
623
+ continue;
624
+ const arrRef = typedNodes.get(n.id);
625
+ const elemRef = arrRef.elem;
626
+ const decl = findDecl(plan, elemRef.name);
627
+ const intoKey = m.into;
628
+ // over element type (all element fields EXCEPT the into field): these come from the over element
629
+ // struct (the parent collection element). We copy each by same-named field access.
630
+ const overFields = decl.fields.filter((f) => f.name !== intoKey);
631
+ const intoRowRef = mapElemRowRef(n, arrRef, plan);
632
+ const fn = mapElemMaterializerName(comp.name, n.id);
633
+ const overElemGo = mapOverElemType(comp, n, typedNodes, plan);
634
+ const elemRowGo = rawElemStructName(comp.name, n.id);
635
+ const lines = [];
636
+ lines.push(`// ${fn} — assemble the augmented map element '${n.id}': copy the over element's typed`);
637
+ lines.push(`// fields + set the '${intoKey}' (into) field from the concrete per-element handler row.`);
638
+ lines.push(`func ${fn}(over ${overElemGo}, into ${elemRowGo}) ${elemRef.name} {`);
639
+ lines.push(`\tvar out ${elemRef.name}`);
640
+ for (const f of overFields) {
641
+ lines.push(`\tout.${goFieldName(f.name)} = over.${goFieldName(f.name)}`);
642
+ }
643
+ lines.push(emitRowCopy(`out.${goFieldName(intoKey)}`, "into", intoRowRef, plan, 1));
644
+ lines.push(`\treturn out`);
645
+ lines.push(`}`);
646
+ parts.push(lines.join("\n"));
647
+ }
648
+ return parts.join("\n\n");
649
+ }
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) {
658
+ const m = node.map;
659
+ const e = classifyExpr(m.over);
660
+ if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
661
+ const baseRef = typedNodes.get(e.path[0]);
662
+ const { expr, ref } = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
663
+ if (ref.kind === "arr")
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
+ }
673
+ }
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);
679
+ }
680
+ function classifyPort(node) {
681
+ if (staticArrElems(node) !== null)
682
+ return "static";
683
+ const e = classifyExpr(node);
684
+ switch (e.kind) {
685
+ case "str":
686
+ case "bool":
687
+ case "null":
688
+ case "ref":
689
+ return "static";
690
+ case "concat":
691
+ return e.parts.every((p) => classifyPort(reconstructG(p)) === "static") ? "static" : "dynamic";
692
+ default:
693
+ return "dynamic";
694
+ }
695
+ }
696
+ function reconstructG(e) {
697
+ switch (e.kind) {
698
+ case "str":
699
+ return e.value;
700
+ case "bool":
701
+ return e.value;
702
+ case "null":
703
+ return null;
704
+ case "ref":
705
+ return { [e.opt ? "refOpt" : "ref"]: e.path };
706
+ case "concat":
707
+ return { concat: e.parts.map(reconstructG) };
708
+ case "dynamic":
709
+ return e.node;
710
+ }
711
+ }
712
+ // bc#90 / runtime-free: the old nvVE-based `emitStaticVE` / `valueLeaf` port lowering (which built
713
+ // `dslcontracts.Value` leaves via the nv* helpers) is REMOVED — every covered port now lowers to a
714
+ // FULLY NATIVE Go value in emitPortInit (string / bool / static-string-array / number literal), and a
715
+ // genuinely-dynamic port fails closed. There is no boxed-Value fallback on the covered read plane.
716
+ // ── typed input struct (In_<comp>) + NATIVE scalar port lowering ─────────────────────────────
717
+ //
718
+ // A covered runner takes a CONCRETE per-component input struct `In_<comp>` (fields = the component's
719
+ // inputPorts with their declared Go types) instead of a generic *Obj. A simple string/scalar port —
720
+ // a literal, a ref to a typed input field (`in.UserId`), a ref to a prior node's typed struct scalar
721
+ // field, or a concat of statically-string parts — then lowers DIRECTLY to a native Go expression
722
+ // (`"USER#" + in.UserId`) assigned straight into the concrete ports-struct field: NO dslcontracts.Value,
723
+ // NO nvVE/nvBindVE helper, NO `.(string)` type-assert. The value is statically the concrete type (the
724
+ // input schema / prior-node outType guarantee it, and literals are self-evidently typed), so dropping
725
+ // the runtime TYPE_MISMATCH check is semantics-preserving. A port that CANNOT be statically proven the
726
+ // concrete type (a ref to a non-matching-typed input, a refOpt / opt field, or a dynamic operator)
727
+ // falls back to the nvVE Value path (documented — the only place a `.(T)` could survive; the covered
728
+ // string key/table/projection ports never hit it).
729
+ /** In_<comp> — the concrete per-component input struct name (fields = inputPorts). */
730
+ function inStructName(compName) {
731
+ return `In_${sanitize(compName)}`;
732
+ }
733
+ /** Go type for an input port's declared type (scalar → concrete; array+elemType → []ElemT; else Value). */
734
+ function inputPortGoType(schema, plan) {
735
+ switch (schema?.type) {
736
+ case "string":
737
+ return "string";
738
+ case "int":
739
+ return "int64";
740
+ case "float":
741
+ return "float64";
742
+ case "bool":
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
+ }
753
+ default:
754
+ return `${PKG}.Value`;
755
+ }
756
+ }
757
+ /** The scalar kind an input port declares, or undefined if it is not a native scalar. */
758
+ function inputScalarKind(schema) {
759
+ switch (schema?.type) {
760
+ case "string":
761
+ return "string";
762
+ case "int":
763
+ return "int64";
764
+ case "float":
765
+ return "float64";
766
+ case "bool":
767
+ return "bool";
768
+ default:
769
+ return undefined;
770
+ }
771
+ }
772
+ /** emitInStruct — the concrete input struct declaration for a covered component (fields = inputPorts,
773
+ * declaration order = Object key order; gofmt-aligned). Empty inputPorts => empty struct. */
774
+ function emitInStruct(comp, plan) {
775
+ const name = inStructName(comp.name);
776
+ const ports = Object.keys(comp.inputPorts ?? {});
777
+ if (ports.length === 0) {
778
+ return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
779
+ }
780
+ const names = ports.map((p) => goFieldName(p));
781
+ const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan));
782
+ const nameW = Math.max(0, ...names.map((n) => n.length));
783
+ const typeW = Math.max(0, ...types.map((t) => t.length));
784
+ const fields = ports
785
+ .map((p, i) => `\t${names[i].padEnd(nameW)} ${types[i].padEnd(typeW)} // ${JSON.stringify(p)}`)
786
+ .join("\n");
787
+ return `// ${name} — the CONCRETE input for '${comp.name}' (fields = inputPorts; typed, consumer-built —
788
+ // NO generic *Obj, NO per-field boxing crosses the covered read boundary).
789
+ type ${name} struct {
790
+ ${fields}
791
+ }`;
792
+ }
793
+ /**
794
+ * emitInDecoder — the TEST-glue decoder `decode_In_<comp>(input *Obj) (In_<comp>, error)`. It reads
795
+ * each declared input port from the generic *Obj into the concrete In_<comp> field (scalar → typed
796
+ * read with a `.(T)` on the SDK/wire Value; Value-typed field → the raw Value). Lives ONLY in the
797
+ * observe companion (test harness), never in the covered module — the covered runner takes In_<comp>
798
+ * pre-built by the consumer. A missing key leaves the zero value (every covered input is required +
799
+ * always supplied by the corpus); a present-but-wrong-scalar is a fail-closed decode error.
800
+ */
801
+ function emitInDecoder(comp, plan) {
802
+ const name = inStructName(comp.name);
803
+ const ports = Object.keys(comp.inputPorts ?? {});
804
+ const lines = [];
805
+ lines.push(`func decode_${name}(input *${PKG}.Obj) (${name}, error) {`);
806
+ lines.push(`\tvar in ${name}`);
807
+ if (ports.length === 0 || comp.inputPorts === undefined) {
808
+ lines.push(`\t_ = input`);
809
+ lines.push(`\treturn in, nil`);
810
+ lines.push(`}`);
811
+ return lines.join("\n");
812
+ }
813
+ lines.push(`\tif input == nil {`);
814
+ lines.push(`\t\treturn in, nil`);
815
+ lines.push(`\t}`);
816
+ for (const p of ports) {
817
+ const field = goFieldName(p);
818
+ const schema = comp.inputPorts[p];
819
+ const scalar = inputScalarKind(schema);
820
+ const et = schema.elemType;
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
+ }
840
+ if (scalar === undefined) {
841
+ lines.push(`\t\tin.${field} = v`);
842
+ }
843
+ else {
844
+ lines.push(`\t\tt, tok := v.(${scalar})`);
845
+ lines.push(`\t\tif !tok {`);
846
+ lines.push(`\t\t\treturn in, ${PKG}.NewExprFailure("TYPE_MISMATCH", "input ${p}: expected ${scalar}, got "+${PKG}.TypeName(v))`);
847
+ lines.push(`\t\t}`);
848
+ lines.push(`\t\tin.${field} = t`);
849
+ }
850
+ lines.push(`\t}`);
851
+ }
852
+ lines.push(`\treturn in, nil`);
853
+ lines.push(`}`);
854
+ return lines.join("\n");
855
+ }
856
+ /**
857
+ * emitNativeScalar — lower a static port expr to a NATIVE Go expression of the concrete `want` scalar
858
+ * type, or null if it cannot be statically proven that type. Literals: a string literal is native for
859
+ * `want==="string"`, a bool literal for `want==="bool"`. A ref resolves via `resolveRef` (typed input
860
+ * field / prior-node scalar / element binding). A concat lowers ONLY into `string` and only when EVERY
861
+ * part is itself a native string (so the concat's strings-only TYPE_MISMATCH can never fire — the parts
862
+ * are statically strings), emitting `p0 + p1 + …`. Returns null (fall back to the Value path) otherwise.
863
+ */
864
+ function emitNativeScalar(node, want, resolveRef) {
865
+ // a static arr is a Value port (never a scalar) — not native-lowerable here.
866
+ if (staticArrElems(node) !== null)
867
+ return null;
868
+ const e = classifyExpr(node);
869
+ switch (e.kind) {
870
+ case "str":
871
+ return want === "string" ? JSON.stringify(e.value) : null;
872
+ case "bool":
873
+ return want === "bool" ? (e.value ? "true" : "false") : null;
874
+ case "null":
875
+ return null; // a null literal is a Value(nil), never a required native scalar
876
+ case "ref": {
877
+ const sc = resolveRef(e.path[0], e.path.slice(1), e.opt);
878
+ if (sc === null || sc.scalar !== want)
879
+ return null;
880
+ return sc.expr;
881
+ }
882
+ case "concat": {
883
+ if (want !== "string")
884
+ return null;
885
+ const parts = [];
886
+ for (const p of e.parts) {
887
+ const sub = emitNativeScalar(reconstructG(p), "string", resolveRef);
888
+ if (sub === null)
889
+ return null;
890
+ // parenthesize a `+` sub-expression so string concatenation binds correctly (a ref/literal is
891
+ // atomic; a nested concat is already a single `a + b` — wrap to keep it one operand).
892
+ parts.push(sub.includes(" + ") ? `(${sub})` : sub);
893
+ }
894
+ return parts.join(" + ");
895
+ }
896
+ default:
897
+ return null;
898
+ }
899
+ }
900
+ /**
901
+ * emitPortInit — emit the port field build for one port as a FULLY NATIVE Go expression (bc#90 /
902
+ * runtime-free). Every covered port lowers to a concrete Go value assigned straight into the ports
903
+ * struct field — NO `dslcontracts.Value`, NO nvVE helper, NO `.(T)` type-assert:
904
+ * - a static string array (projection) → `[]string{"a", "b", ...}` (change #2);
905
+ * - a bare int/float number literal (e.g. Query `limit: 20`) → `int64(20)` / `float64(...)`;
906
+ * - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
907
+ * A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered
908
+ * plane — a boxed escape would re-introduce the bc-runtime coupling this module removes). Returns the
909
+ * struct field initializer (`Field: <expr>`). `resolveRef` resolves a ref head for the scalar path.
910
+ */
911
+ function emitPortInit(pn, expr, fieldGoType, resolveRef) {
912
+ const fieldName = goPortFieldName(pn);
913
+ // change #2: a static string array (projection) → a native `[]string{...}` literal.
914
+ const strArr = staticStringArrElems(expr);
915
+ if (strArr !== null) {
916
+ if (fieldGoType !== "[]string") {
917
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90): port '${pn}' is a static string array but its field type is '${fieldGoType}' (expected []string).`);
918
+ }
919
+ return `${fieldName}: []string{${strArr.map((s) => JSON.stringify(s)).join(", ")}}`;
920
+ }
921
+ // a bare number literal (e.g. Query `limit: 20`): the SSoT range check ran in numLiteralGoExpr —
922
+ // emit the concrete native int64/float64 (unwrap the dslcontracts.Value(...) box: it was only there
923
+ // for the old boxed path; the range check is what matters, and it already passed).
924
+ const num = numLiteralGoExpr(expr);
925
+ if (num !== null) {
926
+ if (fieldGoType === "int64")
927
+ return `${fieldName}: int64(${expr})`;
928
+ if (fieldGoType === "float64")
929
+ return `${fieldName}: float64(${expr})`;
930
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90): port '${pn}' is a number literal but its field type is '${fieldGoType}'.`);
931
+ }
932
+ // string / bool / scalar-input / concat → native scalar expression (no Value, no `.(T)`).
933
+ const scalarWant = fieldGoType;
934
+ const native = emitNativeScalar(expr, scalarWant, resolveRef);
935
+ if (native !== null) {
936
+ return `${fieldName}: ${native}`;
937
+ }
938
+ // FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would
939
+ // re-couple the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the
940
+ // component is not native-eligible and must regenerate on the boxed straight-line path.
941
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): port '${pn}' (${JSON.stringify(expr)}) does not lower to a native Go value of type '${fieldGoType}'. The covered read plane admits NO boxed dslcontracts.Value escape — regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
942
+ }
943
+ /**
944
+ * emitParallelStageArm (bc#87) — EXPLICIT static parallel orchestration for a real-concurrency stage.
945
+ * The stage's members are statically known (from plan.groups), each a plain componentRef point read.
946
+ * We reproduce plan.go's bounded-parallel protocol EXACTLY, so the observed value / emitted op multiset
947
+ * / failure PRECEDENCE / skip reps byte-match run_behavior:
948
+ *
949
+ * 1. PREFLIGHT (ascending index): for each member, decide run/skip from the SETTLED prior-stage
950
+ * results (a plain `produced_<parent>` gate — members are mutually independent, §1) and build its
951
+ * native ports struct. The ports are computed BEFORE dispatch (never inside the goroutine) so the
952
+ * order-of-evaluation is deterministic and a port ExprFailure surfaces in ascending index order.
953
+ * 2. DISPATCH (bounded): one `go func(){…}()` per RUNNABLE member, bound to the static plan.concurrency
954
+ * via a semaphore channel of known size (min(concurrency, member count)); a sync.WaitGroup joins
955
+ * them. Each goroutine ONLY calls the handler and stores its RawOutcome into a per-member struct
956
+ * local — the goroutine spawn is the SOLE runtime element.
957
+ * 3. COMMIT (ascending index): interpret Policy Kind + materialize the RawValue into the outType
958
+ * struct, in ascending index order. Under 'fail'/'retry', the FIRST (lowest-index) failing member
959
+ * wins — a race can NEVER change which failure surfaces, because interpretation is committed
960
+ * deterministically after all execs settle (collect-then-apply, mirroring plan.go's commit loop).
961
+ *
962
+ * NO RunPlan / []OpSpec / planSpec walk: the staging, the bound, and which ops are parallel are baked.
963
+ */
964
+ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags, resolveRefAt) {
965
+ const lines = [];
966
+ const ids = stage.map((i) => comp.body[i].id);
967
+ lines.push(`\t// ── bc#87 real-concurrency stage {${ids.map((x) => JSON.stringify(x)).join(", ")}} — static parallel`);
968
+ lines.push(`\t// orchestration (goroutine per op, bound=${concurrency}); preflight+commit in ascending index order.`);
969
+ lines.push(`\t{`);
970
+ // per-member outcome + resolved-flag + ports locals, declared at STAGE scope (assigned in the
971
+ // ascending preflight, read at dispatch/commit — a goroutine closes over the ports local).
972
+ for (const i of stage) {
973
+ const s = sanitize(comp.body[i].id);
974
+ const structName = portsStructName(comp.name, comp.body[i].id);
975
+ const rowT = rawRowStructName(comp.name, comp.body[i].id);
976
+ lines.push(`\t\tvar row_${s} ${rowT}`);
977
+ lines.push(`\t\tvar row_${s}Resolved bool`);
978
+ lines.push(`\t\trun_${s} := false`);
979
+ lines.push(`\t\tvar ps_${s} ${structName}`);
980
+ lines.push(`\t\t_ = ps_${s}`);
981
+ }
982
+ // 1) PREFLIGHT (ascending): parent-produced gate + ports build, per member (BEFORE dispatch).
983
+ for (const i of stage) {
984
+ const node = comp.body[i];
985
+ const s = sanitize(node.id);
986
+ const structName = portsStructName(comp.name, node.id);
987
+ let ind = "\t\t";
988
+ const closers = [];
989
+ if (node.parent !== undefined && priorNodeAt(node.parent, atPos)) {
990
+ lines.push(`${ind}if produced_${sanitize(node.parent)} {`);
991
+ closers.unshift(`${ind}}`);
992
+ ind += "\t";
993
+ }
994
+ const inits = [];
995
+ const resolveRef = resolveRefAt(atPos);
996
+ for (const pn of Object.keys(node.ports)) {
997
+ const expr = node.ports[pn];
998
+ const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`);
999
+ inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef));
1000
+ }
1001
+ lines.push(`${ind}ps_${s} = ${structName}{${inits.join(", ")}}`);
1002
+ lines.push(`${ind}run_${s} = true`);
1003
+ for (const c of closers)
1004
+ lines.push(c);
1005
+ }
1006
+ // 2) DISPATCH (bounded): one goroutine per runnable member, bound by a semaphore of the static size.
1007
+ const bound = Math.min(concurrency, stage.length);
1008
+ lines.push(`\t\tvar wg sync.WaitGroup`);
1009
+ lines.push(`\t\tsem := make(chan struct{}, ${bound})`);
1010
+ for (const i of stage) {
1011
+ const node = comp.body[i];
1012
+ const s = sanitize(node.id);
1013
+ lines.push(`\t\tif run_${s} {`);
1014
+ lines.push(`\t\t\twg.Add(1)`);
1015
+ lines.push(`\t\t\tsem <- struct{}{}`);
1016
+ lines.push(`\t\t\tgo func() {`);
1017
+ lines.push(`\t\t\t\tdefer wg.Done()`);
1018
+ lines.push(`\t\t\t\tdefer func() { <-sem }()`);
1019
+ lines.push(`\t\t\t\trow_${s}, row_${s}Resolved = handlers.${nodeMethodName(comp.name, node.id)}(ps_${s}, nil)`);
1020
+ lines.push(`\t\t\t}()`);
1021
+ lines.push(`\t\t}`);
1022
+ }
1023
+ lines.push(`\t\twg.Wait()`);
1024
+ // 3) COMMIT (ascending index): interpret + materialize deterministically — lowest-index failure wins.
1025
+ for (const i of stage) {
1026
+ const node = comp.body[i];
1027
+ const meta = metas[i];
1028
+ const s = sanitize(node.id);
1029
+ const ref = typedNodes.get(node.id);
1030
+ const oc = `row_${s}`;
1031
+ lines.push(`\t\tif run_${s} {`);
1032
+ lines.push(`\t\t\tif !${oc}Resolved {`, `\t\t\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${node.component}' has no handler (fail-closed)"`)}`, `\t\t\t}`);
1033
+ if (meta.policy === "continue") {
1034
+ lines.push(`\t\t\tif !${oc}.IsError {`);
1035
+ lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 4));
1036
+ lines.push(`\t\t\t\tproduced_${s} = true`);
1037
+ lines.push(`\t\t\t}`);
1038
+ }
1039
+ else {
1040
+ const policy = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1041
+ lines.push(`\t\t\tif ${oc}.IsError {`);
1042
+ lines.push(`\t\t\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under '${policy}: "+${oc}.Err`)}`);
1043
+ lines.push(`\t\t\t}`);
1044
+ lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 3));
1045
+ lines.push(`\t\t\tproduced_${s} = true`);
1046
+ }
1047
+ lines.push(`\t\t}`);
1048
+ }
1049
+ lines.push(`\t}`);
1050
+ return lines.join("\n");
1051
+ }
1052
+ /**
1053
+ * emitNativeRawRunner — the combined read exec for one component, as a STRUCT-RETURNING runner
1054
+ * (the fully de-plumbed path): native ports struct IN (direct field assignment), raw struct result
1055
+ * OUT (marshalRaw_* straight into the node's outType struct — no boxed Value on the row plane),
1056
+ * INLINE sequential (NO RunPlan). Node results are TYPED STRUCT locals; a relation child reads the
1057
+ * parent's REAL struct result inline (typed field access — no serialize), and the child-present /
1058
+ * bindField / continue-skip decision is made from the real parent value — so relationSingle /
1059
+ * connection converge with run_behavior. The output is assembled as a TYPED STRUCT/value (via the
1060
+ * typed output lowering — struct literal + struct-field access, ZERO NewObj/.Set). For a covered
1061
+ * read whose ports/output are input-static + cross-node refs, this runner allocates ZERO *Obj: the
1062
+ * consumer (graphddb) drives it and keeps the struct native. A Value observation is produced ONLY
1063
+ * by the separate serialize wrapper (run_native_raw_<comp>, below), the single outer Value boundary.
1064
+ */
1065
+ function emitNativeRawRunner(comp, plan, flags) {
1066
+ const ep = execPlan(comp);
1067
+ const order = ep.order;
1068
+ const cplan = buildComponentPlan(comp, goStraightlineInternals.dialect, "scope");
1069
+ const metas = analyzeSequentialOps(cplan.opsLiteral, order);
1070
+ const fn = sanitize(comp.name);
1071
+ const typedNodes = new Map();
1072
+ for (const n of comp.body)
1073
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1074
+ const idToPos = new Map();
1075
+ order.forEach((opIdx, k) => idToPos.set(comp.body[opIdx].id, k));
1076
+ const priorNodeAt = (head, atPos) => {
1077
+ const p = idToPos.get(head);
1078
+ return p !== undefined && p < atPos;
1079
+ };
1080
+ const outRef = comp.outputType !== undefined ? deriveTypeRef(comp.outputType, plan) : undefined;
1081
+ const outStructType = outRef ? renderTypeRef(outRef) : `${PKG}.Value`;
1082
+ const zeroOut = outRef ? goZero(outRef) : "nil";
1083
+ const lines = [];
1084
+ lines.push(`// ${runnerName(comp.name)} — the STRUCT-RETURNING combined read (bc#77/#87): the fully`);
1085
+ lines.push(`// de-plumbed path. Ports are a native struct (direct field assignment); the handler`);
1086
+ lines.push(`// result is materialized straight into the node's outType struct. Node results are typed`);
1087
+ lines.push(`// struct locals; a relation child reads the parent's REAL struct result via direct field`);
1088
+ lines.push(`// access (child-present decision from the real parent value — relationSingle / connection`);
1089
+ lines.push(`// converge). A strictly-sequential plan is a plain statement list (no plan driver); a`);
1090
+ lines.push(`// real-concurrency stage (bc#87) is EXPLICIT static parallel orchestration — one goroutine`);
1091
+ lines.push(`// per statically-known parallel op, bounded by the static plan.concurrency via a semaphore`);
1092
+ lines.push(`// of known size, with a sync.WaitGroup; preflight + interpret are committed in ascending`);
1093
+ lines.push(`// index order so the observed value / op multiset / failure precedence byte-match run_behavior`);
1094
+ lines.push(`// (the goroutine spawn is the ONLY runtime element — the WHAT is baked). The output is a typed`);
1095
+ lines.push(`// struct/value assembled by struct literal + field access — the consumer keeps it native.`);
1096
+ // FULLY CONCRETE: GENERIC over the per-component CONCRETE handler interface ${handlerIfaceName(comp.name)},
1097
+ // whose Node_* methods return the concrete per-node row struct (typed fields = the node's outType).
1098
+ // The covered result plane carries no boxed ports / boxed value / dynamic accessor / semantic type-
1099
+ // assert on a row value — the runner reads row.<Field> directly and copies each into the outType local.
1100
+ lines.push(`func ${runnerName(comp.name)}[H ${handlerIfaceName(comp.name)}](handlers H, in ${inStructName(comp.name)}) (${outStructType}, error) {`);
1101
+ lines.push(`\t_ = in`);
1102
+ // resolveNativeRef for this runner: a ref head is either a PRIOR NODE (read its typed struct field
1103
+ // directly) or an INPUT PORT (read the typed `in.<Field>`). A REQUIRED scalar resolves to a native
1104
+ // Go expression; an opt / non-scalar / refOpt resolves to null (falls back to the nvVE Value path).
1105
+ const resolveRefAt = (atPos) => (head, restPath, opt) => {
1106
+ if (opt)
1107
+ return null; // a refOpt may be nil at runtime — keep the Value path (opt-nil semantics)
1108
+ if (priorNodeAt(head, atPos)) {
1109
+ const baseRef = typedNodes.get(head);
1110
+ let acc;
1111
+ let expr;
1112
+ try {
1113
+ const r = goTypedInternals.typedFieldAccess(typedLocal(head), baseRef, restPath, plan);
1114
+ expr = r.expr;
1115
+ acc = r.ref;
1116
+ }
1117
+ catch {
1118
+ return null;
1119
+ }
1120
+ if (acc.kind !== "scalar" || acc.scalar === "null")
1121
+ return null;
1122
+ return { expr, scalar: goScalar(acc.scalar) };
1123
+ }
1124
+ if (restPath.length !== 0)
1125
+ return null; // input.<field> path is a Value walk — not a native scalar
1126
+ const scalar = inputScalarKind(comp.inputPorts?.[head]);
1127
+ if (scalar === undefined)
1128
+ return null;
1129
+ return { expr: `in.${goFieldName(head)}`, scalar };
1130
+ };
1131
+ for (const k of order.keys()) {
1132
+ const id = comp.body[order[k]].id;
1133
+ lines.push(`\tvar ${typedLocal(id)} ${renderTypeRef(typedNodes.get(id))}`);
1134
+ lines.push(`\tproduced_${sanitize(id)} := false`);
1135
+ lines.push(`\t_ = ${typedLocal(id)}`);
1136
+ lines.push(`\t_ = produced_${sanitize(id)}`);
1137
+ }
1138
+ const idToPosAll = new Map();
1139
+ order.forEach((opIdx, k) => idToPosAll.set(opIdx, k));
1140
+ for (let k = 0; k < order.length; k++) {
1141
+ const i = order[k];
1142
+ // bc#87: a real-concurrency stage — emit static parallel orchestration for ALL its members here,
1143
+ // then advance past them. The stage members are consecutive in `order` (execPlan concatenates
1144
+ // ascending-sorted stages), so we emit the block at the first member and skip the rest.
1145
+ const stage = ep.parallelStageOf.get(i);
1146
+ if (stage !== undefined && stage[0] === i) {
1147
+ lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags, resolveRefAt));
1148
+ k += stage.length - 1;
1149
+ continue;
1150
+ }
1151
+ // #86 pt2: a covered batched map...into node — struct-native element collection + into enrichment.
1152
+ if ("map" in comp.body[i]) {
1153
+ lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut, flags));
1154
+ continue;
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
+ }
1163
+ const node = comp.body[i];
1164
+ const meta = metas[i];
1165
+ const s = sanitize(node.id);
1166
+ const structName = portsStructName(comp.name, node.id);
1167
+ const portNames = Object.keys(node.ports);
1168
+ const ref = typedNodes.get(node.id);
1169
+ lines.push(`\t// ── op '${node.id}' (${node.component}${node.relationKind ? ", relationKind:" + node.relationKind : ""}${node.parent ? ", parent:" + node.parent : ""}) ──`);
1170
+ let indent = "\t";
1171
+ const closers = [];
1172
+ if (node.parent !== undefined && priorNodeAt(node.parent, k)) {
1173
+ lines.push(`${indent}if produced_${sanitize(node.parent)} {`);
1174
+ closers.unshift(`${indent}}`);
1175
+ indent += "\t";
1176
+ if (node.bindField !== undefined) {
1177
+ // bind (change #3 — typed produced-aware bound): read the parent struct field DIRECTLY (typed
1178
+ // access — no serialize). `bound_<s>` is a TYPED POINTER (`*T`) — nil => UNPRODUCED/skip, non-nil
1179
+ // => produced (an empty string is a VALID produced value, a non-nil *string to ""). This is the
1180
+ // EXACT `!= nil` skip-gate ≡ run_behavior's preflightOp, now runtime-free (NO dslcontracts.Value).
1181
+ const parentRef = typedNodes.get(node.parent);
1182
+ const { expr: bfExpr, ref: bfRef } = goTypedInternals.typedFieldAccess(typedLocal(node.parent), parentRef, [node.bindField], plan);
1183
+ if (bfRef.kind === "opt") {
1184
+ // an opt bindField is ALREADY a *T: nil => skip, non-nil => produced. Use it directly.
1185
+ lines.push(`${indent}bound_${s} := ${bfExpr}`);
1186
+ }
1187
+ else {
1188
+ // a required scalar bindField is always PRESENT (never skips on null): take its address so
1189
+ // the produced pointer is non-nil, preserving the "produced" semantics of the old guard.
1190
+ lines.push(`${indent}boundV_${s} := ${bfExpr}`);
1191
+ lines.push(`${indent}bound_${s} := &boundV_${s}`);
1192
+ }
1193
+ lines.push(`${indent}if bound_${s} != nil {`);
1194
+ closers.unshift(`${indent}}`);
1195
+ indent += "\t";
1196
+ }
1197
+ }
1198
+ // ports: build the native struct by direct field assignment (bc#90 / runtime-free). Every covered
1199
+ // port lowers to a concrete Go value (literal / input-param ref / cross-node typed field / concat /
1200
+ // static string array / number literal) assigned straight into the ports field — ZERO
1201
+ // dslcontracts.Value, ZERO nvVE helper, ZERO `.(T)`. A genuinely-dynamic port FAILS CLOSED.
1202
+ const inits = [];
1203
+ const boundArg = node.bindField !== undefined ? `bound_${s}` : "nil";
1204
+ const resolveRef = resolveRefAt(k);
1205
+ for (const pn of portNames) {
1206
+ const expr = node.ports[pn];
1207
+ const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`);
1208
+ inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef));
1209
+ }
1210
+ lines.push(`${indent}ports_${s} := ${structName}{${inits.join(", ")}}`);
1211
+ const oc = `row_${s}`;
1212
+ lines.push(`${indent}${oc}, ${oc}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg})`);
1213
+ lines.push(`${indent}if !${oc}Resolved {`, `${indent}\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${node.component}' has no handler (fail-closed)"`)}`, `${indent}}`);
1214
+ const copy = (ind) => emitRowCopy(typedLocal(node.id), oc, ref, plan, ind.length);
1215
+ if (meta.policy === "continue") {
1216
+ lines.push(`${indent}if !${oc}.IsError {`);
1217
+ lines.push(copy(indent + "\t"));
1218
+ lines.push(`${indent}\tproduced_${s} = true`);
1219
+ lines.push(`${indent}}`);
1220
+ }
1221
+ else {
1222
+ const policy = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1223
+ lines.push(`${indent}if ${oc}.IsError {`);
1224
+ lines.push(`${indent}\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under '${policy}: "+${oc}.Err`)}`);
1225
+ lines.push(`${indent}}`);
1226
+ lines.push(copy(indent));
1227
+ lines.push(`${indent}produced_${s} = true`);
1228
+ }
1229
+ for (const c of closers)
1230
+ lines.push(c);
1231
+ }
1232
+ // output: assemble a TYPED struct/value via the typed output lowering (struct literal + typed
1233
+ // field access — ZERO NewObj/.Set). This is the reused boxed-typed output assembly, but the
1234
+ // result stays a struct (the consumer keeps it native). An input-param output ref would need the
1235
+ // Value-scope fallback (scopeNR) — the covered read corpus does not hit it.
1236
+ //
1237
+ // #86 part 1: for a bindField relationSingle child that can SKIP at runtime, a ref/refOpt into it
1238
+ // must yield the UNPRODUCED rep (run_behavior's writeUnproduced: null for a single child) rather
1239
+ // than the zero-value struct. So when any covered node canSkip, we use the produced-aware lowering
1240
+ // which gates such a ref on the node's produced_<id> flag.
1241
+ const skippable = new Set();
1242
+ const skippableKind = new Map();
1243
+ for (const k of order.keys()) {
1244
+ const bodyNode = comp.body[order[k]];
1245
+ const id = bodyNode.id;
1246
+ if (metas[order[k]].canSkip) {
1247
+ skippable.add(id);
1248
+ // relationKind of the skippable node decides its unproduced rep (#93 shape 1): a connection
1249
+ // yields the empty connection {items:[],cursor:null}; single/absent yields null (opt-nil).
1250
+ const rk = "map" in bodyNode ? bodyNode.map.relationKind : bodyNode.relationKind;
1251
+ skippableKind.set(id, rk);
1252
+ }
1253
+ }
1254
+ const out = skippable.size > 0
1255
+ ? emitProducedAwareValue(comp.output, outRef, typedNodes, plan, skippable, skippableKind)
1256
+ : emitTypedValue(comp.output, outRef, typedNodes, plan);
1257
+ lines.push(`\treturn ${out.expr}, nil`);
1258
+ lines.push(`}`);
1259
+ return { code: lines.join("\n"), outRef: out.ref ?? outRef };
1260
+ }
1261
+ // bc#90 / runtime-free: the old nvVE-based `emitMapStaticVE` map-element port lowering is REMOVED —
1262
+ // covered map element ports lower to a fully native Go value in emitPortInit (the map resolveRef reads
1263
+ // the $as element / prior-node / input typed fields directly), with a fail-closed on a dynamic port.
1264
+ /**
1265
+ * emitMapArm — the struct-native exec of a covered map node. Three shapes:
1266
+ * - #86 pt2: BATCHED map...into (nestedBatchGet) — dispatch once, zip-attach `into` onto each over
1267
+ * element (the augmented element materializer).
1268
+ * - #93 shape 2: NON-batched map...into — dispatch the child handler ONCE PER ELEMENT (N physical
1269
+ * requests, matching run_behavior's per-element loop), materialize `into` per element.
1270
+ * - #93 shape 3: BATCHED map with NO `into` — dispatch once, collect each element DIRECTLY from the
1271
+ * aligned batch raw via the element's own named marshaller (the connection/plain-list collect).
1272
+ * In every case: over is a typed field access (no serialize), element ports are a native struct
1273
+ * (direct field assignment; $as refs read the over element's typed fields), and the result is a
1274
+ * native []T with NO boxed Value on any plane. The map is under the parent's produced gate (inline).
1275
+ */
1276
+ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, flags) {
1277
+ const m = node.map;
1278
+ const batched = m.batched === true;
1279
+ const hasInto = m.into !== undefined;
1280
+ const hasGuard = m.when !== undefined;
1281
+ const s = sanitize(node.id);
1282
+ const structName = portsStructName(comp.name, node.id);
1283
+ const batchStruct = `${structName}_batch`;
1284
+ const arrRef = typedNodes.get(node.id);
1285
+ const elemGo = renderTypeRef(arrRef);
1286
+ const asName = m.as;
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);
1291
+ const elemMat = mapElemMaterializerName(comp.name, node.id);
1292
+ const priorHere = (head) => priorNodeAt(head, atPos);
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
+ };
1311
+ // emit the per-element native ports struct build (shared; `il` is the indent inside the element
1312
+ // loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
1313
+ const buildPorts = (il, elemVar) => {
1314
+ const out = [];
1315
+ const inits = [];
1316
+ // resolveNativeRef for a map element port: a ref head is the element binding ($as → the over element
1317
+ // typed struct field `elemVar.<Field>`), a PRIOR NODE (its typed struct field), or an INPUT PORT
1318
+ // (`in.<Field>`). A REQUIRED scalar lowers to a native Go expr; opt / non-scalar → null (Value path).
1319
+ const resolveRef = (head, restPath, opt) => {
1320
+ if (opt)
1321
+ return null;
1322
+ let baseExpr;
1323
+ let baseRef;
1324
+ if (head === asName) {
1325
+ baseExpr = elemVar;
1326
+ baseRef = overElemRef;
1327
+ }
1328
+ else if (priorHere(head)) {
1329
+ baseExpr = typedLocal(head);
1330
+ baseRef = typedNodes.get(head);
1331
+ }
1332
+ else {
1333
+ if (restPath.length !== 0)
1334
+ return null;
1335
+ const scalar = inputScalarKind(comp.inputPorts?.[head]);
1336
+ if (scalar === undefined)
1337
+ return null;
1338
+ return { expr: `in.${goFieldName(head)}`, scalar };
1339
+ }
1340
+ let acc;
1341
+ let expr;
1342
+ try {
1343
+ const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
1344
+ expr = r.expr;
1345
+ acc = r.ref;
1346
+ }
1347
+ catch {
1348
+ return null;
1349
+ }
1350
+ if (acc.kind !== "scalar" || acc.scalar === "null")
1351
+ return null;
1352
+ return { expr, scalar: goScalar(acc.scalar) };
1353
+ };
1354
+ const asBinding = { name: asName, ref: overElemRef, plan };
1355
+ for (const pn of portNames) {
1356
+ const fieldGoType = portFieldGoType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding);
1357
+ inits.push(emitPortInit(pn, m.ports[pn], fieldGoType, resolveRef));
1358
+ }
1359
+ out.push(`${il}ep_${s} := ${structName}{${inits.join(", ")}}`);
1360
+ return out;
1361
+ };
1362
+ const lines = [];
1363
+ const closers = [];
1364
+ lines.push(`\t// ── map '${node.id}' (${m.component}, ${batched ? "batched" : "per-element"}, into:${JSON.stringify(m.into ?? null)}${m.relationKind ? ", relationKind:" + m.relationKind : ""}${m.parent ? ", parent:" + m.parent : ""}) ──`);
1365
+ let indent = "\t";
1366
+ // the map is gated on the parent's produced flag (inline, like a relation child).
1367
+ if (m.parent !== undefined && priorNodeAt(m.parent, atPos)) {
1368
+ lines.push(`${indent}if produced_${sanitize(m.parent)} {`);
1369
+ closers.unshift(`${indent}}`);
1370
+ indent += "\t";
1371
+ }
1372
+ // over collection (typed field access to the parent node's arr — no serialize).
1373
+ lines.push(`${indent}over_${s} := ${overExpr}`);
1374
+ lines.push(`${indent}${typedLocal(node.id)} = make(${elemGo}, 0, len(over_${s}))`);
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
+ };
1390
+ if (batched) {
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.
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}))`);
1398
+ lines.push(`${indent}for _, ${elemLocal(s)} := range over_${s} {`);
1399
+ for (const l of emitGuard(indent + "\t", elemLocal(s)))
1400
+ lines.push(l);
1401
+ for (const l of buildPorts(indent + "\t", elemLocal(s)))
1402
+ lines.push(l);
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)})`);
1406
+ lines.push(`${indent}}`);
1407
+ // empty items => empty result (handler not called), matching run_behavior.
1408
+ lines.push(`${indent}if len(items_${s}) > 0 {`);
1409
+ lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: items_${s}}`);
1410
+ lines.push(`${indent}\tmo_${s}, mo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
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}`);
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}`);
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}`);
1414
+ if (hasInto) {
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}`);
1420
+ }
1421
+ else {
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 {`);
1424
+ lines.push(`${indent}\t\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
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}`);
1428
+ }
1429
+ lines.push(`${indent}}`);
1430
+ }
1431
+ else {
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_.
1435
+ lines.push(`${indent}for mk_${s} := range over_${s} {`);
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);
1439
+ for (const l of buildPorts(indent + "\t", elemLocal(s)))
1440
+ lines.push(l);
1441
+ // per-element dispatch (ONE physical request per element). run_behavior passes the over element
1442
+ // as the bound value; the element data is fully conveyed by the native ports struct here, so bound
1443
+ // stays nil. Byte-equivalence holds because the bound value only feeds handler-internal logic.
1444
+ lines.push(`${indent}\tmo_${s}, mo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(ep_${s}, nil)`);
1445
+ lines.push(`${indent}\tif !mo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${m.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
1446
+ 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}`);
1447
+ if (hasInto) {
1448
+ lines.push(`${indent}\tel_${s} := ${elemMat}(${elemLocal(s)}, mo_${s})`);
1449
+ }
1450
+ else {
1451
+ lines.push(`${indent}\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
1452
+ lines.push(emitRowCopy(`el_${s}`, `mo_${s}`, elemRowRef, plan, indent.length + 1));
1453
+ }
1454
+ lines.push(`${indent}\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1455
+ lines.push(`${indent}}`);
1456
+ }
1457
+ lines.push(`${indent}produced_${s} = true`);
1458
+ for (const c of closers)
1459
+ lines.push(c);
1460
+ void overElemGo;
1461
+ return lines.join("\n");
1462
+ }
1463
+ /** the map element loop variable (the over element, a typed struct). */
1464
+ function elemLocal(s) {
1465
+ return `oel_${s}`;
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
+ }
1537
+ /**
1538
+ * emitProducedAwareValue — the #86 part 1 produced-aware output lowering (go). It mirrors the
1539
+ * boxed-typed emitTypedValue, but a `ref`/`refOpt` whose HEAD is a skippable relationSingle child is
1540
+ * gated on that node's `produced_<id>` flag: when the child is UNPRODUCED (bound parent field was
1541
+ * null/absent), run_behavior's writeUnproduced makes the node null, so a refOpt into it yields null
1542
+ * (the output opt field => nil). We emit that exactly: `if produced_<head> { <typed field access> }
1543
+ * else { <unproduced-rep for the expected type> }`. Obj/arr/concat/literal recurse; a ref into a
1544
+ * NON-skippable node delegates to the plain typed field access (identical to emitTypedValue).
1545
+ */
1546
+ function emitProducedAwareValue(node, expected, typedNodes, plan, skippable, skippableKind) {
1547
+ // ref / refOpt (classifyExpr encodes both as kind:"ref" with .opt).
1548
+ const e = classifyExpr(node);
1549
+ if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
1550
+ const head = e.path[0];
1551
+ const baseRef = typedNodes.get(head);
1552
+ if (skippable.has(head)) {
1553
+ // produced-aware: gate the field access on produced_<head>. The unproduced branch yields the
1554
+ // node's unproduced rep per run_behavior's writeUnproduced (unproducedValue(relationKind)):
1555
+ // - single / absent → null (opt-nil);
1556
+ // - connection (#93 shape 1) → the empty connection {items:[],cursor:null}. Because a WHOLE
1557
+ // connection node has no field path (e.path.length===1), the unproduced rep is the struct
1558
+ // zero of the connection struct (its serializer normalizes a nil items slice to [] and the
1559
+ // nil cursor to null) — matching unproducedValue("connection") byte-for-byte.
1560
+ const { expr, ref } = goTypedInternals.typedFieldAccess(typedLocal(head), baseRef, e.path.slice(1), plan);
1561
+ const producedFlag = `produced_${sanitize(head)}`;
1562
+ const producedExpr = valueLeafTyped(expr, ref, e.opt, expected);
1563
+ const outTy = expected ? renderTypeRef(expected) : renderTypeRef(ref);
1564
+ const isConn = skippableKind.get(head) === "connection" && e.path.length === 1;
1565
+ // For a connection whole-node ref, the unproduced rep is the connection struct zero (empty
1566
+ // items + nil cursor), even if the output field is opt (refOpt): run_behavior always writes the
1567
+ // empty connection, never null. For single/field-access, the expected-type zero (nil for opt).
1568
+ const unproduced = isConn ? goConnectionUnproduced(ref, expected) : goZero(expected ?? ref);
1569
+ return {
1570
+ expr: `func() ${outTy} { if ${producedFlag} { return ${producedExpr} }; return ${unproduced} }()`,
1571
+ ref: expected ?? ref,
1572
+ };
1573
+ }
1574
+ // non-skippable node ref — identical to emitTypedValue's typed field access.
1575
+ const { expr, ref } = goTypedInternals.typedFieldAccess(typedLocal(head), baseRef, e.path.slice(1), plan);
1576
+ return { expr, ref };
1577
+ }
1578
+ // obj-assembly: recurse each field with the produced-aware lowering (the struct fields may each
1579
+ // ref a skippable node). Only handle the covered named-struct obj output.
1580
+ if (node !== null && typeof node === "object" && !Array.isArray(node)) {
1581
+ const keys = Object.keys(node);
1582
+ if (keys.length === 1 && keys[0] === "obj" && expected && expected.kind === "named") {
1583
+ const decl = findDecl(plan, expected.name);
1584
+ const fields = node.obj;
1585
+ const inits = decl.fields.map((f) => {
1586
+ const sub = emitProducedAwareValue(fields[f.name], f.type, typedNodes, plan, skippable, skippableKind);
1587
+ return `${goFieldName(f.name)}: ${sub.expr}`;
1588
+ });
1589
+ return { expr: `${expected.name}{${inits.join(", ")}}`, ref: expected };
1590
+ }
1591
+ }
1592
+ // everything else (literal / non-skippable ref / obj-of-non-named / arr / concat) — delegate to
1593
+ // the plain typed lowering (no skippable head reachable there in the covered corpus).
1594
+ return emitTypedValue(node, expected, typedNodes, plan);
1595
+ }
1596
+ /** Lower a typed field-access expr to the EXPECTED output type (used in the produced branch). When
1597
+ * the expected type is `opt` and the accessed value is already a *T pointer (the field itself is
1598
+ * opt), pass it through; when the accessed value is a bare leaf and expected is opt, take its addr. */
1599
+ function valueLeafTyped(expr, ref, opt, expected) {
1600
+ void opt;
1601
+ if (expected && expected.kind === "opt") {
1602
+ if (ref.kind === "opt")
1603
+ return expr; // *T already
1604
+ // wrap a bare value in a fresh pointer (matches renderTypeRef(opt)=*T).
1605
+ return `func() ${renderTypeRef(expected)} { __v := ${expr}; return &__v }()`;
1606
+ }
1607
+ return expr;
1608
+ }
1609
+ /**
1610
+ * goConnectionUnproduced — the unproduced rep for a SKIPPED relationKind:connection node (#93 shape
1611
+ * 1): the empty connection {items:[],cursor:null}. The connection outType is a named struct whose
1612
+ * serializer normalizes a nil items slice to [] and a nil cursor to null, so the struct ZERO
1613
+ * (`Tn{}`) serializes to exactly unproducedValue("connection"). When the output field is opt
1614
+ * (refOpt into the connection), run_behavior still writes the empty connection (never null), so we
1615
+ * wrap the struct zero in a fresh pointer rather than yielding nil.
1616
+ */
1617
+ function goConnectionUnproduced(nodeRef, expected) {
1618
+ const connRef = expected && expected.kind === "opt" ? expected.inner : (expected ?? nodeRef);
1619
+ const zero = goZero(connRef);
1620
+ if (expected && expected.kind === "opt") {
1621
+ return `func() ${renderTypeRef(expected)} { __c := ${zero}; return &__c }()`;
1622
+ }
1623
+ return zero;
1624
+ }
1625
+ /** Go zero value for a TypeRef (used as the error return of the struct runner). */
1626
+ function goZero(ref) {
1627
+ switch (ref.kind) {
1628
+ case "named":
1629
+ return `${ref.name}{}`;
1630
+ case "scalar":
1631
+ switch (ref.scalar) {
1632
+ case "string":
1633
+ return `""`;
1634
+ case "int":
1635
+ return "0";
1636
+ case "float":
1637
+ return "0";
1638
+ case "bool":
1639
+ return "false";
1640
+ case "null":
1641
+ return "nil";
1642
+ }
1643
+ return "nil";
1644
+ case "arr":
1645
+ return "nil";
1646
+ case "opt":
1647
+ return "nil";
1648
+ }
1649
+ }
1650
+ // bc#90 / runtime-free: the nv* helper set (nvVE/nvBindVE/nvFieldVE/nvCatVE/nvArrVE) is REMOVED —
1651
+ // those were dslcontracts.Value-based; the covered plane now lowers every port to a native Go value
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
+ }
1834
+ // ── BindNativeRaw dispatch surface ──────────────────────────────────────────────────────
1835
+ /** The struct dispatch surface: ComponentNamesNativeRaw + the per-component struct runners
1836
+ * (RunNativeRawStruct_<comp>) ARE the exposed API. The consumer calls the struct runner directly
1837
+ * (it returns the typed model — NO Value serialization on the read hot path, matching how graphddb
1838
+ * reads: a model, never a boxed Value). There is deliberately NO Value dispatch map — that would
1839
+ * require a Value serializer (the boxing residue bc#77 eliminates). A canonical Value observation
1840
+ * (equivalence pin) is produced by the SEPARATE test-only observe companion (goTypedNativeObserve),
1841
+ * which lives OUTSIDE this module so the covered module stays literal whole-file zero. */
1842
+ function emitStructSurface(native) {
1843
+ return `// ComponentNamesNativeRaw — covered reads exposed on the combined struct-native path (declaration
1844
+ // order). Each is driven via RunNativeRawStruct_<comp>(handlers, input) -> (T, error): a STRUCT
1845
+ // return; the consumer keeps the model native (no Value serialization on the read hot path — the
1846
+ // boxing residue bc#77 removes). See INTEGRATION.md §6.
1847
+ var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name)).join(", ")}}`;
1848
+ }
1849
+ /** Minimal module header for a FULLY-covered module (bc#90 / runtime-free). A fully-covered module
1850
+ * imports ZERO bc runtime (only the std-lib packages it genuinely uses: `fmt` for a batched-map
1851
+ * mismatch Sprintf, `strings` for a concat builder, `sync` for a parallel stage). ExpectedSpecVersions
1852
+ * stays as a local `var` for provenance, but the old `dslcontracts.SpecVersions` skew `init()` is GONE
1853
+ * (change #5 — there is no runtime to skew against once the module is runtime-free). */
1854
+ function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath) {
1855
+ const sv = ctx.specVersions;
1856
+ const imports = [];
1857
+ if (needFmt)
1858
+ imports.push(`\t"fmt"`);
1859
+ if (needMath)
1860
+ imports.push(`\t"math"`);
1861
+ if (needStrings)
1862
+ imports.push(`\t"strings"`);
1863
+ if (needSync)
1864
+ imports.push(`\t"sync"`);
1865
+ const importBlock = imports.length > 0 ? `\nimport (\n${imports.join("\n")}\n)\n` : "";
1866
+ return `// GENERATED by behavior-contracts (bc#90, go-typed-native — the RUNTIME-FREE covered read de-box) — DO NOT EDIT.
1867
+ // Input: portable component-graph IR only. Every component here is a COVERED READ (sequential typed
1868
+ // componentRef); it is emitted ONLY as the combined native ports IN + raw struct OUT + inline
1869
+ // sequential runner below. There is NO generic Bind/dispatch/plan scaffolding AND NO bc-runtime import —
1870
+ // a fully-covered module is FULLY NATIVE (zero bc-runtime reference; only the std-lib it uses).
1871
+
1872
+ package behaviors
1873
+ ${importBlock}
1874
+ // ExpectedSpecVersions are the spec versions baked at generation time (provenance only — the module
1875
+ // is runtime-free, so there is no runtime SpecVersions to skew-check against, bc#90 / change #5).
1876
+ var ExpectedSpecVersions = map[string]int64{"behavior": ${sv.behavior}, "expression": ${sv.expression}, "plan": ${sv.plan}}`;
1877
+ }
1878
+ // ── module assembly ──────────────────────────────────────────────────────────────────
1879
+ function emit(ctx) {
1880
+ // Fail closed if a covered read SHAPE lacks the typed annotations (no silent re-box).
1881
+ for (const c of ctx.ir.components)
1882
+ assertTypedCoverage(c);
1883
+ const native = ctx.ir.components.filter(isNativeRaw);
1884
+ const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
1885
+ const plan = buildTypePlan(ctx.ir);
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;
1890
+ // Emit the runners first so the nv* helper usage flags are known.
1891
+ const flags = { bind: false, field: false, cat: false, arr: false };
1892
+ const structs = [];
1893
+ const inStructs = [];
1894
+ const rowStructs = [];
1895
+ const handlerIfaces = [];
1896
+ const elemCopiers = [];
1897
+ const runnerCodes = [];
1898
+ for (const c of native) {
1899
+ inStructs.push(emitInStruct(c, plan));
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));
1904
+ for (const n of c.body) {
1905
+ if ("map" in 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).
1912
+ }
1913
+ else {
1914
+ structs.push(emitPortsStruct(c, n));
1915
+ }
1916
+ }
1917
+ // CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
1918
+ rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
1919
+ handlerIfaces.push(emitHandlerInterface(c, typedNodes, plan));
1920
+ // augmented-element copiers for covered map...into nodes (over fields + concrete into row).
1921
+ const mm = emitMapElemMaterializers(c, typedNodes, plan);
1922
+ if (mm)
1923
+ elemCopiers.push(mm);
1924
+ const r = emitNativeRawRunner(c, plan, flags);
1925
+ runnerCodes.push(r.code);
1926
+ }
1927
+ const structSurface = emitStructSurface(native);
1928
+ const behaviorErrorType = emitBehaviorErrorType();
1929
+ // bc#87: a covered component with a real-concurrency stage emits sync.WaitGroup + a semaphore.
1930
+ const needSync = native.some((c) => concurrencyPlan(c) !== null);
1931
+ // bc#90: the ONLY std-lib `fmt.Sprintf` site on the covered plane is the batched-map result-count
1932
+ // mismatch error — so `fmt` is imported iff a covered component has a batched map node.
1933
+ const needFmt = native.some((c) => c.body.some((n) => "map" in n && n.map.batched === true));
1934
+ // The FULLY-covered module (no non-native components) gets a MINIMAL, RUNTIME-FREE header — NO
1935
+ // bc-runtime import at all (bc#90).
1936
+ // FAIL CLOSED (bc#86/#89): the --typed-native (native codegen) surface is native-only. If ANY
1937
+ // component is not a covered native read, we do NOT silently box it through the straight-line
1938
+ // emitter (the old concealment) — we throw a LOUD GeneratorFailure naming every uncovered component
1939
+ // and its specific reject reason. A fully-covered module (nonNative.length === 0) is unchanged: the
1940
+ // minimal, runtime-free header.
1941
+ if (nonNative.length > 0) {
1942
+ const rejects = nonNative
1943
+ .map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
1944
+ .join("\n");
1945
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (native codegen) surface: ${nonNative.length} component(s) are NOT a covered native read:\n${rejects}\n\n` +
1946
+ `The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
1947
+ `box uncovered shapes. Implement native coverage for this shape, fix the spec if the shape is ` +
1948
+ `genuinely runtime-dynamic, or generate this component on the --ir dynamic interpreter surface.`);
1949
+ }
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);
1954
+ const banner = `// COMBINED READ layer (bc#90 — the RUNTIME-FREE read de-box; STRUCT-ONLY, FULLY-NATIVE surface). A
1955
+ // covered read is a strictly-sequential typed componentRef chain (point reads + relationKind:single
1956
+ // children), emitted ONLY here: each node builds a native ports struct by direct field assignment (every
1957
+ // port is a CONCRETE Go value — string / bool / []string projection / int64 limit — NO boxed Value, NO
1958
+ // PortReader accessor); the CONCRETE per-component handler interface (Handler_<comp>) takes a typed
1959
+ // produced-aware 'bound' pointer and returns a CONCRETE per-node row struct (Row_<comp>_<node>, typed
1960
+ // fields = the node's outType) whose fields the runner reads DIRECTLY and copies into the node's outType
1961
+ // struct. The covered plane carries NO bc-runtime reference at all — failures use the LOCAL
1962
+ // *BehaviorError (same codes, byte-equal to run_behavior). Exec is INLINE sequential (no plan driver) so
1963
+ // a relation child reads the parent's REAL struct result via direct field access and the child-present
1964
+ // decision is made from the real parent value — relationSingle / connection converge with run_behavior
1965
+ // (fixes #323/#74). The exposed API is the STRUCT-returning RunNativeRawStruct_<comp> (the consumer
1966
+ // keeps the model native — no serialization on the read hot path). This module is FULLY NATIVE: a naive
1967
+ // grep for boxing primitives OR for the bc-runtime package finds nothing, by design (bc#90/runtime-free).`;
1968
+ const sections = [
1969
+ banner,
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}`,
1971
+ decls ? `// Typed struct declarations (outType-derived; hash-dedup — shared type plan).\n${decls}` : undefined,
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")}`,
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,
1976
+ `// Per-component CONCRETE handler interfaces (one typed Node_* method per covered node).\n${handlerIfaces.join("\n\n")}`,
1977
+ elemCopiers.length
1978
+ ? `// Augmented map-element copiers (over element fields + the concrete into row — pure assignment).\n${elemCopiers.join("\n\n")}`
1979
+ : undefined,
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,
1986
+ `// Combined read runners (STRUCT-returning — the fully de-plumbed path).\n${runnerCodes.join("\n\n")}`,
1987
+ structSurface,
1988
+ ].filter((s) => !!s);
1989
+ return `${header}\n\n${sections.join("\n\n")}\n`;
1990
+ }
1991
+ /**
1992
+ * goTypedNativeObserve — the TEST-ONLY observe companion (a SEPARATE file in the SAME package as the
1993
+ * covered module). It carries the typed->Value serializers (ser_*) + an ObserveNativeRaw dispatch
1994
+ * that drives the module-private struct runner and serializes its result to a canonical Value for
1995
+ * the equivalence pin. This lives OUTSIDE the covered module so the module stays literal whole-file
1996
+ * zero (the ser_* NewObj is test glue, never on the consumer's read hot path). Emitted only for the
1997
+ * fully-covered case (the equivalence harness); production consumers never use it.
1998
+ */
1999
+ export function goTypedNativeObserve(ir, runtimeImport) {
2000
+ const components = ir.components;
2001
+ const native = components.filter(isNativeRaw);
2002
+ const plan = buildTypePlan(ir);
2003
+ const serializers = emitSerializers(plan);
2004
+ const marshallers = goTypedInternals.emitMarshallers(plan);
2005
+ const mustHelpers = emitMustHelpers(plan);
2006
+ // arr/opt node materialize helpers (mustArr_*/mustOpt_*) for the concrete-row decode of a node whose
2007
+ // whole outType is a bare arr/opt (materializeExpr emits mustArr_/mustOpt_ for those).
2008
+ const arrOptHelpers = goTypedInternals.emitArrOptNodeHelpers({ ir }, plan);
2009
+ const arms = native
2010
+ .map((c) => {
2011
+ const outRef = c.outputType !== undefined ? deriveTypeRef(c.outputType, plan) : undefined;
2012
+ const ser = outRef ? serializeTyped("tv", outRef, plan) : "tv";
2013
+ // decode the generic *Obj input into the CONCRETE In_<comp> struct (test glue — ONE decode at the
2014
+ // top, OUTSIDE the covered hot path; a REQUIRED declared input is present so an absent key is a
2015
+ // fail-closed decode error surfaced here, never on the native runner path).
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}`;
2017
+ })
2018
+ .join("\n");
2019
+ // input decoders (TEST glue): generic *Obj -> concrete In_<comp>. Scalar fields read the typed value
2020
+ // (a `.(T)` here is on the SDK/wire Value in the OBSERVE companion, never in the covered module).
2021
+ const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
2022
+ // The observe companion drives EVERY covered component, so its handler type param H must satisfy
2023
+ // EVERY per-component concrete Handler_<comp> interface — a combined constraint embeds them all.
2024
+ const combinedConstraint = `allNativeRawHandlers`;
2025
+ const constraintDecl = `// ${combinedConstraint} — the combined constraint embedding every covered component's concrete
2026
+ // handler interface, so one handler value drives all covered runners (test glue only).
2027
+ type ${combinedConstraint} interface {
2028
+ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
2029
+ }`;
2030
+ // ── concrete scripted adapter (TEST glue): implements every Node_* method by draining a per-
2031
+ // COMPONENT scripted-outcome queue and DECODING the scripted ok Value into the CONCRETE row struct.
2032
+ // This is where the generic scripted source (a Value queue) meets the concrete per-node seam: the
2033
+ // decode is generated (it knows the concrete row type), so the runner stays fully concrete. ──
2034
+ const decodeFns = [];
2035
+ const methodImpls = [];
2036
+ const emittedDecode = new Set();
2037
+ for (const c of native) {
2038
+ const typedNodes = new Map();
2039
+ for (const n of c.body)
2040
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
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.
2044
+ const ref = typedNodes.get(n.id);
2045
+ if ("map" in n) {
2046
+ const m = n.map;
2047
+ const batched = m.batched === true;
2048
+ const arrRef = ref;
2049
+ const elemRowRef = mapElemRowRef(n, arrRef, plan);
2050
+ const elemT = rawElemStructName(c.name, n.id);
2051
+ const batchT = rawRowStructName(c.name, n.id);
2052
+ const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2053
+ const portsT = batched ? `${portsStructName(c.name, n.id)}_batch` : portsStructName(c.name, n.id);
2054
+ const retT = batched ? batchT : elemT;
2055
+ const mapBoundT = boundGoType(n, c, typedNodes, plan);
2056
+ if (batched) {
2057
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${batchT}, bool) {
2058
+ src, ok := s.next(${JSON.stringify(m.component)})
2059
+ if !ok {
2060
+ return ${batchT}{}, false
2061
+ }
2062
+ if src.isErr {
2063
+ return ${batchT}{IsError: true, Err: src.err}, true
2064
+ }
2065
+ arr, _ := src.ok.([]${PKG}.Value)
2066
+ rows := make([]${elemT}, 0, len(arr))
2067
+ for _, ev := range arr {
2068
+ rows = append(rows, ${elemDecode}(ev))
2069
+ }
2070
+ return ${batchT}{Rows: rows}, true
2071
+ }`);
2072
+ }
2073
+ else {
2074
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${elemT}, bool) {
2075
+ src, ok := s.next(${JSON.stringify(m.component)})
2076
+ if !ok {
2077
+ return ${elemT}{}, false
2078
+ }
2079
+ if src.isErr {
2080
+ return ${elemT}{IsError: true, Err: src.err}, true
2081
+ }
2082
+ return ${elemDecode}(src.ok), true
2083
+ }`);
2084
+ }
2085
+ void retT;
2086
+ continue;
2087
+ }
2088
+ const node = n;
2089
+ const rowT = rawRowStructName(c.name, node.id);
2090
+ const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
2091
+ const nodeBoundT = boundGoType(node, c, typedNodes, plan);
2092
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, node.id)}(_ ${portsStructName(c.name, node.id)}, _ ${nodeBoundT}) (${rowT}, bool) {
2093
+ src, ok := s.next(${JSON.stringify(node.component)})
2094
+ if !ok {
2095
+ return ${rowT}{}, false
2096
+ }
2097
+ if src.isErr {
2098
+ return ${rowT}{IsError: true, Err: src.err}, true
2099
+ }
2100
+ return ${decode}(src.ok), true
2101
+ }`);
2102
+ }
2103
+ }
2104
+ const adapter = `// scriptSrc — a scripted per-component outcome (an ok Value or an error), drained in order.
2105
+ type scriptSrc struct {
2106
+ isErr bool
2107
+ err string
2108
+ ok ${PKG}.Value
2109
+ }
2110
+
2111
+ // ScriptedNativeRaw — the CONCRETE test handler: it satisfies every Handler_<comp> interface by
2112
+ // draining a per-COMPONENT scripted queue and decoding the ok Value into the concrete row struct.
2113
+ // The decode (decodeRow_*) is generated, so the covered runner drives fully-concrete Node_* methods.
2114
+ // EXPORTED so the runner glue (a separate package) can name it for the generic instantiation.
2115
+ type ScriptedNativeRaw struct {
2116
+ mu sync.Mutex
2117
+ queues map[string][]scriptSrc
2118
+ seen int
2119
+ }
2120
+
2121
+ // NewScriptedNativeRaw builds the concrete test handler from the JSON handlers spec (the SAME shape
2122
+ // the reference scriptedHandlers reads): { "<component>": {ok|error} | [ {ok|error}, … ] }. Each
2123
+ // component's ok Value is DecodeValue'd once here (test glue); the per-node method decodes it into the
2124
+ // concrete row. Returns the handler + any decode error (surfaced by the runner as a fatal).
2125
+ func NewScriptedNativeRaw(spec *${PKG}.JObj) (*ScriptedNativeRaw, error) {
2126
+ queues := map[string][]scriptSrc{}
2127
+ if spec != nil {
2128
+ for _, comp := range spec.Keys {
2129
+ var entries []${PKG}.JNode
2130
+ switch t := spec.Vals[comp].(type) {
2131
+ case []${PKG}.JNode:
2132
+ entries = t
2133
+ default:
2134
+ entries = []${PKG}.JNode{t}
2135
+ }
2136
+ for _, e := range entries {
2137
+ eo, _ := e.(*${PKG}.JObj)
2138
+ if okNode, has := eo.Get("ok"); has {
2139
+ val, derr := ${PKG}.DecodeValue(okNode)
2140
+ if derr != nil {
2141
+ return nil, derr
2142
+ }
2143
+ queues[comp] = append(queues[comp], scriptSrc{ok: val})
2144
+ continue
2145
+ }
2146
+ ev, _ := eo.Get("error")
2147
+ es, _ := ev.(string)
2148
+ queues[comp] = append(queues[comp], scriptSrc{isErr: true, err: es})
2149
+ }
2150
+ }
2151
+ }
2152
+ return &ScriptedNativeRaw{queues: queues}, nil
2153
+ }
2154
+
2155
+ // next drains one scripted outcome for a component (last entry repeats — mirrors the reference
2156
+ // scriptedHandlers), returns ok=false when the component has no scripted queue (fail-closed). The
2157
+ // bc#87 parallel-stage runner calls Node_* from multiple goroutines, so the drain is mutex-guarded.
2158
+ func (s *ScriptedNativeRaw) next(component string) (scriptSrc, bool) {
2159
+ s.mu.Lock()
2160
+ defer s.mu.Unlock()
2161
+ s.seen++
2162
+ q, ok := s.queues[component]
2163
+ if !ok || len(q) == 0 {
2164
+ return scriptSrc{}, false
2165
+ }
2166
+ if len(q) > 1 {
2167
+ v := q[0]
2168
+ s.queues[component] = q[1:]
2169
+ return v, true
2170
+ }
2171
+ return q[0], true
2172
+ }`;
2173
+ return `// GENERATED test-only observe companion (bc#77) — NOT part of the covered module. Serializes the
2174
+ // struct-native runner's result to a canonical Value for the equivalence pin, and carries the CONCRETE
2175
+ // scripted test handler (scriptedNativeRaw) that satisfies every Handler_<comp> interface by decoding a
2176
+ // scripted Value into the concrete per-node row. The consumer never uses this (it keeps the model
2177
+ // native + implements Handler_<comp> over its own wire payload); it exists only so the test harness can
2178
+ // byte-compare the STRUCT-only covered read against run_behavior. Same package as the covered module.
2179
+ package behaviors
2180
+
2181
+ import (
2182
+ "sync"
2183
+
2184
+ ${PKG} "${runtimeImport ?? "github.com/foo-ogawa/behavior-contracts/go"}"
2185
+ )
2186
+
2187
+ // typed -> Value serializers (TEST glue only — the covered module has none).
2188
+ ${serializers}
2189
+
2190
+ // Value -> typed struct marshallers (+ must* helpers) for the concrete-row decode (TEST glue only).
2191
+ ${marshallers}
2192
+
2193
+ ${mustHelpers}
2194
+
2195
+ ${arrOptHelpers}
2196
+
2197
+ // per-node concrete-row decoders (scripted Value -> RawRow_/RawElem_ struct).
2198
+ ${decodeFns.join("\n\n")}
2199
+
2200
+ // input decoders (generic *Obj -> concrete In_<comp>; TEST glue, one decode at the observe boundary).
2201
+ ${inDecoders}
2202
+
2203
+ ${adapter}
2204
+
2205
+ ${constraintDecl}
2206
+
2207
+ // scriptedNativeRaw method impls (one per covered node — the concrete Node_* seam, test glue).
2208
+ ${methodImpls.join("\n\n")}
2209
+
2210
+ // ObserveNativeRaw drives the struct-native runner and serializes to a canonical Value (test only).
2211
+ // GENERIC over the CONCRETE combined handler type H — so each enclosed RunNativeRawStruct_* call is
2212
+ // instantiated at the concrete handler type (direct, devirtualized per-node method calls), not at an
2213
+ // interface. Instantiate with your concrete handler type to preserve the covered path's concreteness.
2214
+ func ObserveNativeRaw[H ${combinedConstraint}](name string, handlers H, input *${PKG}.Obj) (${PKG}.Value, error) {
2215
+ ${arms}
2216
+ return nil, ${PKG}.NewBehaviorFailure("UNKNOWN_ENTRY", "component '"+name+"' is not a covered read (fail-closed)")
2217
+ }
2218
+ `;
2219
+ }
2220
+ /** emitDecodeRow — emit (once, deduped) a decoder `decodeRow_<comp>_<node><suffix>(v Value) <rowT>`
2221
+ * that materializes the scripted ok Value into the concrete row struct (typed fields = the outType /
2222
+ * elem-row type). Named: reuse the Value->struct marshaller + copy fields; scalar/arr/opt: materialize
2223
+ * into .Val. TEST glue only (the production consumer decodes its own wire payload). */
2224
+ function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix) {
2225
+ const fn = `decodeRow_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}${suffix}`;
2226
+ if (emitted.has(fn))
2227
+ return fn;
2228
+ emitted.add(fn);
2229
+ const lines = [];
2230
+ lines.push(`func ${fn}(v ${PKG}.Value) ${rowT} {`);
2231
+ lines.push(`\tvar out ${rowT}`);
2232
+ if (ref.kind === "named") {
2233
+ const decl = findDecl(plan, ref.name);
2234
+ lines.push(`\tt := ${goTypedInternals.materializeExpr("v", ref, plan)}`);
2235
+ for (const f of decl.fields) {
2236
+ lines.push(`\tout.${goFieldName(f.name)} = t.${goFieldName(f.name)}`);
2237
+ }
2238
+ }
2239
+ else {
2240
+ lines.push(`\tout.Val = ${goTypedInternals.materializeExpr("v", ref, plan)}`);
2241
+ }
2242
+ lines.push(`\treturn out`);
2243
+ lines.push(`}`);
2244
+ out.push(lines.join("\n"));
2245
+ return fn;
2246
+ }
2247
+ /**
2248
+ * goTypedNativeEmitter — language id `go-typed-native` (bc#77). Emits the generic straight-line
2249
+ * body (for non-covered components / helpers) plus the COMBINED READ layer (native ports struct
2250
+ * IN + raw struct result OUT + inline sequential exec) and the BindNativeRaw surface. Requires
2251
+ * outType/outputType on every covered read (fail-closed otherwise).
2252
+ */
2253
+ export const goTypedNativeEmitter = {
2254
+ language: "go-typed-native",
2255
+ fileExtension: ".go",
2256
+ defaultRuntimeImport: "github.com/foo-ogawa/behavior-contracts/go/coderuntime",
2257
+ filenameHint: "behaviors.typed.native.generated.go",
2258
+ emit,
2259
+ };
2260
+ //# sourceMappingURL=emit-typed-native-go.js.map