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,2123 @@
1
+ import { GeneratorFailure } from "./core.js";
2
+ import { rustStraightlineInternals } from "./emit-shared-rust.js";
3
+ import { rustTypedInternals } from "./emit-shared-rust-typed.js";
4
+ import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
5
+ import { concurrencyPlan, execPlan, analyzeSequentialOps, classifyExpr } from "./straightline.js";
6
+ import { NativeExprCompiler } from "./native-expr.js";
7
+ import { buildAsyncPlans } from "./async-plan.js";
8
+ const { sanitize, rustStrLit } = rustStraightlineInternals;
9
+ const { rustFieldName, renderTypeRef, typedCell, typedFieldAccess, serializeTyped, emitTypeDecls, emitDefaults, emitSerializers, emitTypedValue, emitMarshallers, emitMarshalHelpers, materializeExpr, } = rustTypedInternals;
10
+ /** rustErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
11
+ * plane (runtime-free). The covered module declares a local `BehaviorError` (see emitBehaviorErrorType);
12
+ * the runner returns `Result<_, BehaviorError>` over THAT local type. This REPLACES the old
13
+ * `behavior_contracts::BehaviorError::new(...)` (the last error-plane runtime coupling) — the SAME codes
14
+ * are preserved verbatim so the observed failure is byte-equal to run_behavior. */
15
+ function rustErr(code, msgExpr) {
16
+ return `BehaviorError::new(${JSON.stringify(code)}, ${msgExpr})`;
17
+ }
18
+ /** emitBehaviorErrorType — the LOCAL concrete error type baked into the covered module so a runner can
19
+ * fail closed with ZERO bc-runtime import. Codes match run_behavior verbatim (byte-equal). `code()` /
20
+ * `failure_code()` expose the stable code without a bc-runtime type. */
21
+ function emitBehaviorErrorType() {
22
+ return `// BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
23
+ // covered runner returns \`Result<T, BehaviorError>\` over THIS local type instead of a bc-runtime
24
+ // failure, so the fully-covered module imports ZERO bc runtime. Codes match run_behavior verbatim
25
+ // (byte-equal). The observe companion (test glue, a super:: submodule) bridges this local error to the
26
+ // bc-runtime BehaviorError at the observe boundary — the covered module itself stays runtime-free.
27
+ #[derive(Debug, Clone)]
28
+ pub struct BehaviorError {
29
+ pub code: String,
30
+ pub message: String,
31
+ }
32
+
33
+ impl BehaviorError {
34
+ pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
35
+ BehaviorError { code: code.into(), message: message.into() }
36
+ }
37
+ /// The stable failure code (byte-equal to run_behavior) WITHOUT a bc-runtime type.
38
+ pub fn code(&self) -> &str {
39
+ &self.code
40
+ }
41
+ }
42
+
43
+ impl std::fmt::Display for BehaviorError {
44
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45
+ write!(f, "{}: {}", self.code, self.message)
46
+ }
47
+ }
48
+ impl std::error::Error for BehaviorError {}`;
49
+ }
50
+ function portFieldName(wire) {
51
+ const safe = wire.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase();
52
+ return `f_${safe}`;
53
+ }
54
+ function pascal(s) {
55
+ return s.replace(/[^A-Za-z0-9]+/g, " ").split(" ").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") || "X";
56
+ }
57
+ function portsStructName(compName, nodeId) {
58
+ return `PortsNR${pascal(compName)}${pascal(nodeId)}`;
59
+ }
60
+ /** RawRowNR<Comp><Node> — the concrete per-node handler-result row struct name. */
61
+ function rawRowStructName(compName, nodeId) {
62
+ return `RawRowNR${pascal(compName)}${pascal(nodeId)}`;
63
+ }
64
+ /** RawElemNR<Comp><Node> — the concrete per-ELEMENT row struct name for a covered map node. */
65
+ function rawElemStructName(compName, nodeId) {
66
+ return `RawElemNR${pascal(compName)}${pascal(nodeId)}`;
67
+ }
68
+ /** HandlerNR<Comp> — the per-component concrete handler trait name (one node_* method per node). */
69
+ function handlerTraitName(compName) {
70
+ return `HandlerNR${pascal(compName)}`;
71
+ }
72
+ /** node_<id> — the concrete per-node handler method name. */
73
+ function nodeMethodName(nodeId) {
74
+ return `node_${nodeId.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase()}`;
75
+ }
76
+ /** InNR<Comp> — the concrete per-component input struct name (fields = inputPorts). */
77
+ function inStructName(compName) {
78
+ return `InNR${pascal(compName)}`;
79
+ }
80
+ function staticArrElems(node) {
81
+ if (node === null || typeof node !== "object" || Array.isArray(node))
82
+ return null;
83
+ const keys = Object.keys(node);
84
+ if (keys.length !== 1 || keys[0] !== "arr")
85
+ return null;
86
+ const arg = node.arr;
87
+ return Array.isArray(arg) ? arg : null;
88
+ }
89
+ function inferPortType(node, inputPorts, asBinding) {
90
+ if (staticArrElems(node) !== null)
91
+ return "Value";
92
+ const e = classifyExpr(node);
93
+ switch (e.kind) {
94
+ case "str":
95
+ case "concat":
96
+ return "String";
97
+ case "bool":
98
+ return "bool";
99
+ case "ref": {
100
+ // #108: a `$as`-headed ref (map element binding) — resolve the element field's scalar type.
101
+ if (asBinding !== undefined && e.path[0] === asBinding.name) {
102
+ try {
103
+ const { ref } = typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan);
104
+ if (ref.kind === "scalar" && ref.scalar !== "null")
105
+ return rustScalar(ref.scalar);
106
+ }
107
+ catch {
108
+ return "Value";
109
+ }
110
+ return "Value";
111
+ }
112
+ if (e.path.length === 1) {
113
+ const s = inputPorts[e.path[0]];
114
+ if (s) {
115
+ if (s.type === "string")
116
+ return "String";
117
+ if (s.type === "int")
118
+ return "i64";
119
+ if (s.type === "float")
120
+ return "f64";
121
+ if (s.type === "bool")
122
+ return "bool";
123
+ }
124
+ }
125
+ return "Value";
126
+ }
127
+ default:
128
+ return "Value";
129
+ }
130
+ }
131
+ /** A static string-array port (every element a static string literal) — the graphddb `projection`
132
+ * shape. Returns the list of literal strings, or null when the node is not that shape. */
133
+ function staticStringArrElems(node) {
134
+ const arr = staticArrElems(node);
135
+ if (arr === null)
136
+ return null;
137
+ const out = [];
138
+ for (const el of arr) {
139
+ const e = classifyExpr(el);
140
+ if (e.kind !== "str")
141
+ return null;
142
+ out.push(e.value);
143
+ }
144
+ return out;
145
+ }
146
+ /**
147
+ * portFieldRustType — the NATIVE Rust type for a covered port field (bc#90 / runtime-free). The covered
148
+ * ports struct carries NO `Value` field: every port lowers to a concrete Rust type.
149
+ * - a static string-array (projection) → `Vec<&'static str>` (change #2);
150
+ * - a bare int/float number literal (e.g. Query `limit: 20`) → `i64` / `f64`;
151
+ * - a string/bool/scalar-input port → the scalar Rust type (inferPortType).
152
+ * A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
153
+ * Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no `Value`
154
+ * fallback on the native module). `where` names the emission site for the error.
155
+ */
156
+ function portFieldRustType(node, inputPorts, where = "port", asBinding) {
157
+ if (staticStringArrElems(node) !== null)
158
+ return "Vec<&'static str>";
159
+ const num = numLiteralRustExpr(node);
160
+ if (num !== null)
161
+ return typeof node === "number" && Number.isInteger(node) ? "i64" : "f64";
162
+ const ft = inferPortType(node, inputPorts, asBinding);
163
+ if (ft === "Value") {
164
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90 / runtime-free): ${where} '${JSON.stringify(node)}' does not lower to a native Rust type (would need a boxed 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.`);
165
+ }
166
+ return ft;
167
+ }
168
+ /**
169
+ * emitPortsStruct — the CONCRETE native ports struct for a componentRef node. Typed fields per the
170
+ * static port type, constructed directly. NO `impl PortReaderT` / by-name `port()` — the covered runner
171
+ * passes `&PortsNR…` to the concrete `node_*` method by direct generic instantiation, and the handler
172
+ * reads `ports.f_<field>` DIRECTLY. The struct derives Clone (a bc#87 parallel stage clones it into the
173
+ * per-worker dispatch). Every field is a NATIVE Rust type (bc#90 / runtime-free) — NO boxed `Value`.
174
+ */
175
+ function emitPortsStruct(comp, node, asBinding) {
176
+ const name = portsStructName(comp.name, node.id);
177
+ const portNames = Object.keys(node.ports);
178
+ if (portNames.length === 0) {
179
+ return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
180
+ }
181
+ const types = portNames.map((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding));
182
+ const fields = portNames
183
+ .map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
184
+ .join("\n");
185
+ return `// ${name} — CONCRETE native ports for node '${node.id}' (${node.component}). Typed fields per the
186
+ // static port type; constructed directly (no Vec, no heap key strings, no per-port Value boxing). The
187
+ // handler reads the typed fields directly off this concrete struct — no by-name accessor.
188
+ #[derive(Clone)]
189
+ pub struct ${name} {
190
+ ${fields}
191
+ }`;
192
+ }
193
+ /**
194
+ * emitMapPortsStructs — element ports struct + batch struct for a covered map node (CONCRETE). The batch
195
+ * struct carries the Vec of per-element CONCRETE ports structs (the handler reads them off the concrete
196
+ * struct — no by-name accessor, no key-value object).
197
+ */
198
+ function emitMapPortsStructs(comp, node, typedNodes, plan) {
199
+ const m = node.map;
200
+ const name = portsStructName(comp.name, node.id);
201
+ const batch = `${name}Batch`;
202
+ // #108: element ports may read the `$as` binding — resolve the over element type for native typing.
203
+ const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
204
+ const asBinding = { name: m.as, ref: elemRef, plan };
205
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding);
206
+ return `${elemStruct}
207
+
208
+ // ${batch} — CONCRETE batched ports for map '${node.id}': the Vec of per-element CONCRETE ports structs
209
+ // (a Vec<${name}>, NOT a generic key-value container). The consumer's batched handler reads the typed
210
+ // element ports off the concrete struct directly.
211
+ #[derive(Clone)]
212
+ pub struct ${batch} {
213
+ pub items: Vec<${name}>,
214
+ }`;
215
+ }
216
+ // ── native expression compiler backend (#108) — cond `if` / map `when` / cond branches (Rust) ────────
217
+ //
218
+ // The runtime-free native expression compiler (native-expr.ts) mirrors run_behavior's cond/guard with
219
+ // BYTE-EQUAL semantics. The value-dependent runtime failures (INT_OVERFLOW / NAN_OR_INF / MOD_ZERO /
220
+ // PRECISION_LOSS) are emitted as calls to a FIXED set of RUNTIME-FREE helper fns baked into the module
221
+ // (bc_expr_* below) returning Result<T, BehaviorError> — the covered module still imports ZERO bc runtime.
222
+ // The runner is Result<_, BehaviorError>, so a fallible helper propagates with `?`.
223
+ let rustExprTempSeq = 0;
224
+ /** module-level accumulator: set when the native-expr compiler emits a fallible helper call (so emit()
225
+ * bakes the helper library). Reset at the start of each emit(). */
226
+ const rustExprUsed = { helpers: false };
227
+ /** makeRustExprBackend — renders native-expr for Rust. `resolveHead` maps a ref head to an OWNED typed
228
+ * base expr (a prior-node cell clone / an input field / a $as element). Sets rustExprUsed.helpers when
229
+ * a fallible helper is emitted. The runner is Result<_, BehaviorError>, so hoistFallible uses `?`. */
230
+ function makeRustExprBackend(resolveHead, plan) {
231
+ const used = rustExprUsed;
232
+ return {
233
+ lang: "rust",
234
+ freshTemp: () => `ex_${rustExprTempSeq++}`,
235
+ renderType: (ref) => renderTypeRef(ref),
236
+ scalarType: (s) => rustScalar(s),
237
+ strLit: (s) => `${rustStrLit(s)}.to_string()`,
238
+ boolLit: (b) => (b ? "true" : "false"),
239
+ intLit: (dec) => `${dec}i64`,
240
+ floatLit: (n) => `${rustFloatLitExpr(n)}f64`,
241
+ fieldAccess: (baseExpr, baseRef, path) => typedFieldAccess(baseExpr, baseRef, path, plan),
242
+ resolveHead,
243
+ hoistFallible: (temp, _ty, call) => {
244
+ used.helpers = true;
245
+ return [`let ${temp} = ${call}?;`];
246
+ },
247
+ helperCall: (helper, args) => {
248
+ used.helpers = true;
249
+ // the string code-point compare helpers take &str — borrow the owned String args.
250
+ const isStrCmp = helper === "strLt" || helper === "strLe" || helper === "strGt" || helper === "strGe";
251
+ const passed = isStrCmp ? args.map((a) => `&${a}`) : args;
252
+ // lenArr takes &[T] — borrow the owned Vec.
253
+ const finalArgs = helper === "lenArr" ? args.map((a) => `&${a}`) : passed;
254
+ return `bc_expr_${rustSnake(helper)}(${finalArgs.join(", ")})`;
255
+ },
256
+ binOp: (op, a, b) => `(${a} ${op} ${b})`,
257
+ concat: (parts) => {
258
+ // native String concat via a `[&str; N].concat()` — a string-literal part (`"…".to_string()`) is
259
+ // used as the bare `"…"` &str; any other owned String is borrowed with `.as_str()`. This shape is
260
+ // clippy-clean (no to_string_in_format_args / single_char_push_str / unnecessary_to_string).
261
+ const refs = parts.map((p) => {
262
+ const m = /^(".*")\.to_string\(\)$/.exec(p);
263
+ return m ? m[1] : `(${p}).as_str()`;
264
+ });
265
+ return `[${refs.join(", ")}].concat()`;
266
+ },
267
+ notOp: (a) => `(!${a})`,
268
+ structLit: (name, inits) => `${name} { ${inits.map((i) => `${rustFieldName(i.field)}: ${i.expr}`).join(", ")} }`,
269
+ arrLit: (elemTy, items) => `{ let __v: Vec<${elemTy}> = vec![${items.join(", ")}]; __v }`,
270
+ ternary: (cond, t, e, _ty) => `(if ${rustStripOuterParens(cond)} { ${rustStripOuterParens(t)} } else { ${rustStripOuterParens(e)} })`,
271
+ shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
272
+ // and: let temp = if left { <rStmts> rExpr } else { false }; or: if left { true } else { <rStmts> rExpr }.
273
+ // build a block that runs rStmts then yields rExpr (parens stripped — block-tail is not a sub-expr).
274
+ const rblock = `{ ${rStmts.join(" ")} ${rustStripOuterParens(rExpr)} }`;
275
+ if (op === "and") {
276
+ return [`let ${temp}: bool = if ${rustStripOuterParens(left)} ${rblock} else { false };`];
277
+ }
278
+ return [`let ${temp}: bool = if ${rustStripOuterParens(left)} { true } else ${rblock};`];
279
+ },
280
+ condGuard: (temp, ty, cond, tStmts, tExpr, eStmts, eExpr) => {
281
+ const tblock = `{ ${tStmts.join(" ")} ${rustStripOuterParens(tExpr)} }`;
282
+ const eblock = `{ ${eStmts.join(" ")} ${rustStripOuterParens(eExpr)} }`;
283
+ return [`let ${temp}: ${ty} = if ${rustStripOuterParens(cond)} ${tblock} else ${eblock};`];
284
+ },
285
+ };
286
+ }
287
+ /** camelCase helper name → snake_case for the Rust helper fn (addI64 → add_i64, strLt → str_lt). */
288
+ function rustSnake(name) {
289
+ return name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").replace(/([0-9])([a-z])/g, "$1_$2").toLowerCase();
290
+ }
291
+ /** strip ONE redundant outer paren pair from an expr (clippy `if (cond)` unused_parens). Only when the
292
+ * whole string is a single balanced `( … )` — a `(a) && (b)` stays untouched. */
293
+ function rustStripOuterParens(expr) {
294
+ if (!expr.startsWith("(") || !expr.endsWith(")"))
295
+ return expr;
296
+ let depth = 0;
297
+ for (let i = 0; i < expr.length; i++) {
298
+ if (expr[i] === "(")
299
+ depth++;
300
+ else if (expr[i] === ")") {
301
+ depth--;
302
+ if (depth === 0 && i !== expr.length - 1)
303
+ return expr; // closed before the end → not a single wrap
304
+ }
305
+ }
306
+ return expr.slice(1, -1);
307
+ }
308
+ /** Rust float literal (finite; integral gets `.0`). */
309
+ function rustFloatLitExpr(n) {
310
+ if (Number.isInteger(n))
311
+ return `${n}.0`;
312
+ return String(n);
313
+ }
314
+ /**
315
+ * emitExprHelpersRust — the FIXED runtime-free native-expr helper library (Rust). Emitted once per
316
+ * covered module that uses cond/guard/arith. Each helper is BYTE-EQUAL to expr-eval.ts core. Failures
317
+ * are the LOCAL BehaviorError with the SAME code strings as run_behavior.
318
+ */
319
+ function emitExprHelpersRust() {
320
+ return `// ── native-expr runtime-free helper library (#108) — byte-equal to expr-eval.ts core ──
321
+ #[allow(dead_code)]
322
+ fn bc_expr_add_i64(a: i64, b: i64) -> Result<i64, BehaviorError> {
323
+ a.checked_add(b).ok_or_else(|| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
324
+ }
325
+ #[allow(dead_code)]
326
+ fn bc_expr_sub_i64(a: i64, b: i64) -> Result<i64, BehaviorError> {
327
+ a.checked_sub(b).ok_or_else(|| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
328
+ }
329
+ #[allow(dead_code)]
330
+ fn bc_expr_mul_i64(a: i64, b: i64) -> Result<i64, BehaviorError> {
331
+ a.checked_mul(b).ok_or_else(|| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
332
+ }
333
+ #[allow(dead_code)]
334
+ fn bc_expr_neg_i64(a: i64) -> Result<i64, BehaviorError> {
335
+ a.checked_neg().ok_or_else(|| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
336
+ }
337
+ #[allow(dead_code)]
338
+ fn bc_expr_check_finite(v: f64) -> Result<f64, BehaviorError> {
339
+ if v.is_finite() { Ok(v) } else { Err(BehaviorError::new("NAN_OR_INF", "non-finite float")) }
340
+ }
341
+ #[allow(dead_code)]
342
+ fn bc_expr_add_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a + b) }
343
+ #[allow(dead_code)]
344
+ fn bc_expr_sub_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a - b) }
345
+ #[allow(dead_code)]
346
+ fn bc_expr_mul_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a * b) }
347
+ #[allow(dead_code)]
348
+ fn bc_expr_neg_f(a: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(-a) }
349
+ #[allow(dead_code)]
350
+ fn bc_expr_mod_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a % b) }
351
+ #[allow(dead_code)]
352
+ fn bc_expr_mod_i64(a: i64, b: i64) -> Result<i64, BehaviorError> {
353
+ if b == 0 { return Err(BehaviorError::new("MOD_ZERO", "int mod by zero")); }
354
+ // Rust % is truncated (sign of dividend) — matches modCore. MIN % -1 would panic; guard it.
355
+ if a == i64::MIN && b == -1 { return Ok(0); }
356
+ Ok(a % b)
357
+ }
358
+ #[allow(dead_code)]
359
+ fn bc_expr_widen_i64f(a: i64) -> Result<f64, BehaviorError> {
360
+ if !(-9007199254740992..=9007199254740992).contains(&a) {
361
+ return Err(BehaviorError::new("PRECISION_LOSS", "int exceeds exact float range (±2^53)"));
362
+ }
363
+ Ok(a as f64)
364
+ }
365
+ #[allow(dead_code)]
366
+ fn bc_expr_div_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a / b) }
367
+ #[allow(dead_code)]
368
+ fn bc_expr_len_arr<T>(a: &[T]) -> Result<i64, BehaviorError> {
369
+ i64::try_from(a.len()).map_err(|_| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
370
+ }
371
+ // code-point (char) order comparison — matches cmpCodePoints (char-by-char, NOT byte order).
372
+ #[allow(dead_code)]
373
+ fn bc_expr_cmp_cp(a: &str, b: &str) -> std::cmp::Ordering {
374
+ let mut ia = a.chars();
375
+ let mut ib = b.chars();
376
+ loop {
377
+ match (ia.next(), ib.next()) {
378
+ (None, None) => return std::cmp::Ordering::Equal,
379
+ (None, Some(_)) => return std::cmp::Ordering::Less,
380
+ (Some(_), None) => return std::cmp::Ordering::Greater,
381
+ (Some(ca), Some(cb)) => {
382
+ if ca != cb {
383
+ return if (ca as u32) < (cb as u32) { std::cmp::Ordering::Less } else { std::cmp::Ordering::Greater };
384
+ }
385
+ }
386
+ }
387
+ }
388
+ }
389
+ #[allow(dead_code)]
390
+ fn bc_expr_str_lt(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) == std::cmp::Ordering::Less }
391
+ #[allow(dead_code)]
392
+ fn bc_expr_str_le(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) != std::cmp::Ordering::Greater }
393
+ #[allow(dead_code)]
394
+ fn bc_expr_str_gt(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) == std::cmp::Ordering::Greater }
395
+ #[allow(dead_code)]
396
+ fn bc_expr_str_ge(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) != std::cmp::Ordering::Less }`;
397
+ }
398
+ // ── CONCRETE per-node row structs + row→outType copiers (mirror the Go concrete contract) ──────────
399
+ //
400
+ // Each covered node has a CONCRETE row struct `RawRowNR<Comp><Node>` whose fields are the node's projected
401
+ // outType fields (typed, flattened) plus a per-node error signal (is_error/err). A named-struct outType
402
+ // flattens to one Rust field per outType field; a scalar/arr/opt node carries a single `val: <typed>`
403
+ // payload. The per-component `HandlerNR<Comp>` trait has one method per node returning that concrete
404
+ // struct; the runner reads `row.<field>` DIRECTLY (no `RawValue`, no `match`, no `Value::`) and copies
405
+ // each field into the node's outType struct local.
406
+ /** emitRawRowStructs — emit the concrete `RawRowNR<Comp><Node>` struct per covered node (+ per-element
407
+ * `RawElemNR<Comp><Node>` for a covered map). Every row carries the per-node error signal (is_error/err). */
408
+ function emitRawRowStructs(comp, typedNodes, plan) {
409
+ const parts = [];
410
+ for (const n of comp.body) {
411
+ if ("cond" in n)
412
+ continue; // #108: a cond node has no handler-result row (pure Expression).
413
+ const ref = typedNodes.get(n.id);
414
+ if ("map" in n) {
415
+ const arrRef = ref;
416
+ const elemRowRef = mapElemRowRef(n, arrRef, plan);
417
+ parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
418
+ parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
419
+ `#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub is_error: bool,\n pub err: String,\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
420
+ continue;
421
+ }
422
+ parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
423
+ }
424
+ return parts.join("\n\n");
425
+ }
426
+ /** Emit one concrete row struct: flatten a named outType to typed fields, else a single `val` payload;
427
+ * always carry is_error/err. Derives Default so a handler can build an error row cheaply (`..Default`). */
428
+ function emitOneRowStruct(name, ref, plan) {
429
+ const lines = [];
430
+ lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType; + error signal).`);
431
+ lines.push(`#[derive(Default)]`);
432
+ lines.push(`pub struct ${name} {`);
433
+ lines.push(` pub is_error: bool,`);
434
+ lines.push(` pub err: String,`);
435
+ if (ref.kind === "named") {
436
+ const decl = findDecl(plan, ref.name);
437
+ for (const f of decl.fields) {
438
+ lines.push(` pub ${rustFieldName(f.name)}: ${renderTypeRef(f.type)}, // ${JSON.stringify(f.name)}`);
439
+ }
440
+ }
441
+ else {
442
+ lines.push(` pub val: ${renderTypeRef(ref)},`);
443
+ }
444
+ lines.push(`}`);
445
+ return lines.join("\n");
446
+ }
447
+ /** emitRowCopy — the statement copying a concrete row's typed fields into the node's outType local
448
+ * `target` (a value of Rust type renderTypeRef(ref)). Named: struct literal from the row's moved fields;
449
+ * scalar/arr/opt: `.val`. `rowExpr` is the concrete row VALUE (moved). No dynamic read — pure move/copy. */
450
+ function emitRowMove(target, rowExpr, ref, plan) {
451
+ if (ref.kind === "named") {
452
+ const decl = findDecl(plan, ref.name);
453
+ const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: ${rowExpr}.${rustFieldName(f.name)}`).join(", ");
454
+ return `${target} = ${ref.name} { ${inits} };`;
455
+ }
456
+ return `${target} = ${rowExpr}.val;`;
457
+ }
458
+ /**
459
+ * boundRustType (change #3 — typed `bound`) — the CONCRETE Rust type of a covered node's `bound` handler
460
+ * param (bc#90 / runtime-free). `bound` is the parent-produced key (the bindField value); it replaces the
461
+ * old boxed `Value`. We represent PRODUCED-vs-UNPRODUCED with an OWNED `Option<K>`: `Some(v)` = the parent
462
+ * value was produced/present (an empty string is a VALID produced value — `Some("")`); `None` =
463
+ * UNPRODUCED/skip (exactly the old `!= Null` guard ≡ run_behavior). `K` is the bindField's concrete Rust
464
+ * type (a string key here). A node WITHOUT a bindField never receives a bound value — its param is an
465
+ * `Option<String>` placeholder always passed `None` (a typed None, so the ABI stays uniform and carries
466
+ * NO Value).
467
+ */
468
+ function boundRustType(node, typedNodes, plan) {
469
+ if (node.bindField === undefined || node.parent === undefined)
470
+ return "Option<String>";
471
+ const parentRef = typedNodes.get(node.parent);
472
+ if (parentRef === undefined)
473
+ return "Option<String>";
474
+ let bfRef;
475
+ try {
476
+ bfRef = typedFieldAccess(`${typedCell(node.parent)}.borrow()`, parentRef, [node.bindField], plan).ref;
477
+ }
478
+ catch {
479
+ return "Option<String>";
480
+ }
481
+ // opt bindField: the field is already `Option<T>` — the bound is that same Option. A required scalar
482
+ // bindField: the bound is `Option<T>` (always Some — the "produced" pointer is never None).
483
+ if (bfRef.kind === "opt")
484
+ return `Option<${renderTypeRef(bfRef.inner)}>`;
485
+ return `Option<${renderTypeRef(bfRef)}>`;
486
+ }
487
+ /**
488
+ * emitHandlerTrait — the per-component `HandlerNR<Comp>` trait: one concrete-typed `node_*` method per
489
+ * covered node. A componentRef node method takes the node's native ports struct + the typed produced-aware
490
+ * `bound` (change #3 — NO boxed Value) and returns Option<RawRowNR…> (None = unknown component,
491
+ * fail-closed). A covered map node's method takes the batch ports struct (batched) or the element ports
492
+ * struct (per-element). This is the fully-concrete, runtime-free replacement for the NativeRawComponentExec
493
+ * seam.
494
+ */
495
+ function emitHandlerTrait(comp, typedNodes, plan, ap) {
496
+ // bc#97 (increment 2): async I/O model is PER-TERMINAL-NODE. A node whose terminal handler is declared
497
+ // async gets an `async fn node_*` (the consumer's impl returns a future the runner awaits at the call
498
+ // site); a sync terminal stays plain `fn node_*` (called directly, no await). Mixed sync+async in one
499
+ // trait is fine. `async fn` needs NO external runtime to compile — the future is `async fn` sugar over
500
+ // std, so the covered module stays runtime-free either way. Uniform ioModel:'async' ⇒ every method
501
+ // async (byte-identical to increment-1); ioModel unset/'sync' ⇒ every method plain `fn` (pre-#97).
502
+ const fnKwFor = (nodeId) => (ap.nodeIsAsync(nodeId) ? "async fn" : "fn");
503
+ const lines = [];
504
+ lines.push(`// ${handlerTraitName(comp.name)} — the CONCRETE per-component handler seam: one typed method`);
505
+ lines.push(`// per covered node (native ports struct IN, concrete row struct OUT). No generic boxed-ports`);
506
+ lines.push(`// / boxed-value / dynamic accessor crosses the covered boundary — the consumer implements each`);
507
+ lines.push(`// node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
508
+ lines.push(`pub trait ${handlerTraitName(comp.name)} {`);
509
+ for (const n of comp.body) {
510
+ if ("cond" in n)
511
+ continue; // #108: a cond node has no handler method (pure Expression).
512
+ const method = nodeMethodName(n.id);
513
+ const fnKw = fnKwFor(n.id);
514
+ const bt = boundRustType(n, typedNodes, plan);
515
+ if ("map" in n) {
516
+ const m = n.map;
517
+ const batched = m.batched === true;
518
+ const portsT = batched ? `${portsStructName(comp.name, n.id)}Batch` : portsStructName(comp.name, n.id);
519
+ const retT = batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
520
+ lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Option<${retT}>;`);
521
+ continue;
522
+ }
523
+ lines.push(` ${fnKw} ${method}(&self, ports: &${portsStructName(comp.name, n.id)}, bound: ${bt}) -> Option<${rawRowStructName(comp.name, n.id)}>;`);
524
+ }
525
+ lines.push(`}`);
526
+ return lines.join("\n");
527
+ }
528
+ /** mapElemMaterializerName — the augmented-element copier for a covered map...into node. */
529
+ function mapElemMaterializerName(nodeId) {
530
+ return `materialize_map_elem_${typedCell(nodeId)}`;
531
+ }
532
+ /** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
533
+ * type (into) or the element outType directly (no-into). This is the type of RawElemNR<Comp><Node>. */
534
+ function mapElemRowRef(node, arrRef, plan) {
535
+ const m = node.map;
536
+ if (m.into === undefined)
537
+ return arrRef.elem;
538
+ if (arrRef.elem.kind !== "named") {
539
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined map (#86 pt2): node '${node.id}' element outType must be a named struct — regenerate as boxed typed/straight-line.`);
540
+ }
541
+ const decl = findDecl(plan, arrRef.elem.name);
542
+ const intoField = decl.fields.find((f) => f.name === m.into);
543
+ if (!intoField) {
544
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined map (#86 pt2): element struct '${arrRef.elem.name}' has no '${m.into}' (into) field.`);
545
+ }
546
+ return intoField.type;
547
+ }
548
+ /**
549
+ * emitMapElemMaterializers — for each covered map...into node, emit the AUGMENTED-element copier: copy
550
+ * the over element's typed fields + move the `into` field from the concrete per-element handler row
551
+ * (RawElemNR). Pure struct field assignment — NO dynamic value read (the handler already produced the
552
+ * typed into row).
553
+ */
554
+ function emitMapElemMaterializers(comp, typedNodes, plan) {
555
+ const parts = [];
556
+ for (const n of comp.body) {
557
+ if (!("map" in n))
558
+ continue;
559
+ const m = n.map;
560
+ // #93 shape 3: a NO-into map materializes each element directly from the per-element row.
561
+ if (m.into === undefined)
562
+ continue;
563
+ const arrRef = typedNodes.get(n.id);
564
+ if (arrRef.kind !== "arr" || arrRef.elem.kind !== "named") {
565
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined map (#86 pt2): node '${n.id}' element outType must be a named struct — regenerate as boxed typed/straight-line.`);
566
+ }
567
+ const elemRef = arrRef.elem;
568
+ const decl = findDecl(plan, elemRef.name);
569
+ const intoKey = m.into;
570
+ const intoField = decl.fields.find((f) => f.name === intoKey);
571
+ const overFields = decl.fields.filter((f) => f.name !== intoKey);
572
+ const overElemTy = mapOverElemType(comp, n, typedNodes, plan);
573
+ const fn = mapElemMaterializerName(n.id);
574
+ const elemRowGo = rawElemStructName(comp.name, n.id);
575
+ const intoRowRef = mapElemRowRef(n, arrRef, plan);
576
+ const inits = [];
577
+ // Copy scalars (i64/f64/bool) copy directly; String/named/arr/opt clone (clippy clone_on_copy clean).
578
+ for (const f of overFields) {
579
+ const isCopy = f.type.kind === "scalar" && (f.type.scalar === "int" || f.type.scalar === "float" || f.type.scalar === "bool");
580
+ inits.push(` ${rustFieldName(f.name)}: over.${rustFieldName(f.name)}${isCopy ? "" : ".clone()"},`);
581
+ }
582
+ // move the into field off the concrete element row (named → struct literal; scalar/arr/opt → .val).
583
+ const intoValue = intoRowRef.kind === "named"
584
+ ? `${intoRowRef.name} { ${findDecl(plan, intoRowRef.name).fields.map((f) => `${rustFieldName(f.name)}: into.${rustFieldName(f.name)}`).join(", ")} }`
585
+ : `into.val`;
586
+ parts.push(`#[allow(dead_code)]
587
+ fn ${fn}(over: &${overElemTy}, into: ${elemRowGo}) -> ${elemRef.name} {
588
+ ${elemRef.name} {
589
+ ${inits.join("\n")}
590
+ ${rustFieldName(intoKey)}: ${intoValue},
591
+ }
592
+ }`);
593
+ void intoField;
594
+ }
595
+ return parts.join("\n\n");
596
+ }
597
+ /**
598
+ * mapOverElemInfo — resolve a covered map's `over` collection element TypeRef + the OWNED Rust
599
+ * expression that yields the over Vec (#108, rust twin): prior-node arr field (cell clone) or an input
600
+ * array port with elemType (`in_.<field>.clone()`). Fail-closed for an input array without elemType.
601
+ */
602
+ function mapOverElemInfo(comp, node, typedNodes, plan) {
603
+ const m = node.map;
604
+ const e = classifyExpr(m.over);
605
+ if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
606
+ const baseRef = typedNodes.get(e.path[0]);
607
+ const { ref } = typedFieldAccess(`${typedCell(e.path[0])}.borrow()`, baseRef, e.path.slice(1), plan);
608
+ if (ref.kind === "arr") {
609
+ const fieldPath = e.path.slice(1).map((p) => `.${rustFieldName(p)}`).join("");
610
+ return { elemRef: ref.elem, overExpr: `${typedCell(e.path[0])}.borrow()${fieldPath}.clone()` };
611
+ }
612
+ }
613
+ if (e.kind === "ref" && e.path.length === 1) {
614
+ const schema = comp.inputPorts?.[e.path[0]];
615
+ const et = schema?.elemType;
616
+ if (et !== undefined)
617
+ return { elemRef: deriveTypeRef(et, plan), overExpr: `in_.${rustFieldName(e.path[0])}.clone()` };
618
+ }
619
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust 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. BC does not infer input-array element types (consumer-interface C3).`);
620
+ }
621
+ /** The Rust type of the over-element (back-compat shim). */
622
+ function mapOverElemType(comp, node, typedNodes, plan) {
623
+ return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
624
+ }
625
+ // ── eligibility ─────────────────────────────────────────────────────────────────────
626
+ function reconstructR(e) {
627
+ switch (e.kind) {
628
+ case "str":
629
+ return e.value;
630
+ case "bool":
631
+ return e.value;
632
+ case "null":
633
+ return null;
634
+ case "ref":
635
+ return { [e.opt ? "refOpt" : "ref"]: e.path };
636
+ case "concat":
637
+ return { concat: e.parts.map(reconstructR) };
638
+ case "dynamic":
639
+ return e.node;
640
+ }
641
+ }
642
+ /**
643
+ * numLiteralRustExpr — a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
644
+ * `classifyExpr` leaves a bare number `dynamic` (its checked int/float range rules are the evaluate
645
+ * SSoT), so it never reaches the static str/bool/null branch — but a number literal IS statically
646
+ * resolvable to a native `Value`: the SAME range check the runtime applies (integral literal in the
647
+ * safe 2^53-1 window → Value::Int; a fractional/exponent literal → the checked Value::Float) is
648
+ * performed HERE at generation time. Returns the Rust expression for the literal's Value, or null
649
+ * when the node is not a bare number OR an integral literal is out of the safe window (keeping the
650
+ * runtime INVALID_LITERAL semantics — the port is not covered natively, the component falls through).
651
+ * Mirrors behavior_contracts eval of a bare number literal exactly.
652
+ */
653
+ const NUM_SAFE_INT_R = 9007199254740991; // 2^53 - 1 (expr safeInt)
654
+ function numLiteralRustExpr(node) {
655
+ if (typeof node !== "number" || !Number.isFinite(node))
656
+ return null;
657
+ if (Number.isInteger(node)) {
658
+ if (node < -NUM_SAFE_INT_R || node > NUM_SAFE_INT_R)
659
+ return null; // out of safe window → not covered
660
+ return `Value::Int(${node}i64)`;
661
+ }
662
+ // fractional / exponent literal → checked f64 (finite, guaranteed above).
663
+ return `Value::Float(${node}f64)`;
664
+ }
665
+ /** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
666
+ * resolvable to a native Value with no Obj / no evaluate interpreter. */
667
+ function portIsStatic(node) {
668
+ const arr = staticArrElems(node);
669
+ if (arr !== null)
670
+ return arr.every(portIsStatic);
671
+ if (numLiteralRustExpr(node) !== null)
672
+ return true; // bare number literal (e.g. Query `limit`)
673
+ const e = classifyExpr(node);
674
+ switch (e.kind) {
675
+ case "str":
676
+ case "bool":
677
+ case "null":
678
+ case "ref":
679
+ return true;
680
+ case "concat":
681
+ return e.parts.every((p) => portIsStatic(reconstructR(p)));
682
+ default:
683
+ return false;
684
+ }
685
+ }
686
+ /** The output must lower to a typed struct/value with no scope read: a ref to a BODY NODE, an
687
+ * obj/arr of such, or a concat. A ref to an input param / non-concat operator is not lowerable. */
688
+ function outputIsNativeLowerable(comp) {
689
+ const bodyIds = new Set(comp.body.map((n) => n.id));
690
+ const walk = (node) => {
691
+ const arr = staticArrElems(node);
692
+ if (arr !== null)
693
+ return arr.every(walk);
694
+ if (node !== null && typeof node === "object" && !Array.isArray(node)) {
695
+ const keys = Object.keys(node);
696
+ if (keys.length === 1 && keys[0] === "obj") {
697
+ const obj = node.obj;
698
+ return Object.keys(obj).every((k) => walk(obj[k]));
699
+ }
700
+ if (keys.length === 1 && keys[0] === "arr")
701
+ return true;
702
+ }
703
+ const e = classifyExpr(node);
704
+ if (e.kind === "str" || e.kind === "bool" || e.kind === "null")
705
+ return true;
706
+ if (e.kind === "ref")
707
+ return bodyIds.has(e.path[0]);
708
+ if (e.kind === "concat")
709
+ return e.parts.every((p) => walk(reconstructR(p)));
710
+ return false;
711
+ };
712
+ return walk(comp.output);
713
+ }
714
+ /**
715
+ * coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet), rust twin of the go predicate.
716
+ */
717
+ function coveredMapNode(n, bodyIds, comp) {
718
+ if (n === null || typeof n !== "object" || !("map" in n))
719
+ return false;
720
+ const m = n.map;
721
+ // #108 (rust twin of the go predicate): a guarded map (`when`) IS covered with NO `into` (guard+into
722
+ // is heterogeneous — fail-closed). over element type must be statically resolvable: prior-node arr
723
+ // field, or an input array port carrying `elemType` (BC does not infer — consumer-interface C3).
724
+ if (m.when !== undefined && m.into !== undefined)
725
+ return false;
726
+ if (m.policy !== undefined && m.policy !== "fail")
727
+ return false;
728
+ if (m.relationKind !== undefined && m.relationKind !== "single" && m.relationKind !== "connection")
729
+ return false;
730
+ const overE = classifyExpr(m.over);
731
+ if (overE.kind !== "ref")
732
+ return false;
733
+ const head = overE.path[0];
734
+ if (!bodyIds.has(head)) {
735
+ if (overE.path.length !== 1)
736
+ return false;
737
+ const schema = comp.inputPorts?.[head];
738
+ if (schema === undefined || schema.elemType === undefined)
739
+ return false;
740
+ }
741
+ const ports = m.ports;
742
+ for (const p of Object.keys(ports))
743
+ if (!portIsStatic(ports[p]))
744
+ return false;
745
+ return true;
746
+ }
747
+ /** A covered read SHAPE with static ports + native-lowerable output (+ covered map...into), OR a
748
+ * real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point reads. */
749
+ function isSequentialComponentRefRead(comp) {
750
+ return coverageRejectReason(comp) === null;
751
+ }
752
+ /**
753
+ * coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89), rust twin of
754
+ * the go predicate. Returns null when the component IS a covered native read; otherwise a SPECIFIC
755
+ * human-readable reason naming the exact disqualifier and the offending node id. The predicate logic is
756
+ * IDENTICAL to isSequentialComponentRefRead (defined as `coverageRejectReason(c) === null`) so the two
757
+ * can never disagree. This is the message the fail-closed dispatch surfaces per non-native component.
758
+ */
759
+ function coverageRejectReason(comp) {
760
+ const ep = execPlan(comp);
761
+ if (ep === null)
762
+ return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
763
+ for (const [i] of ep.parallelStageOf) {
764
+ const n = comp.body[i];
765
+ if (!("component" in n))
766
+ return `node '${n.id}' is a non-componentRef member of a parallel stage (not native-covered)`;
767
+ if (n.bindField !== undefined)
768
+ return `node '${n.id}' is a bindField relation child inside a parallel stage (not native-covered)`;
769
+ }
770
+ const bodyIds = new Set(comp.body.map((b) => b.id));
771
+ for (const n of comp.body) {
772
+ if ("cond" in n) {
773
+ // #108: a cond node is native-covered (its if/then/else compile via the runtime-free
774
+ // NativeExprCompiler; an uncovered expr fails closed at emit). cond requires outType.
775
+ if (n.outType === undefined)
776
+ return `node '${n.id}' cond has no outType (required to de-box the branch join)`;
777
+ continue;
778
+ }
779
+ if ("map" in n) {
780
+ if (!coveredMapNode(n, bodyIds, comp))
781
+ return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
782
+ continue;
783
+ }
784
+ if (!("component" in n))
785
+ return `node '${n.id}' is not a componentRef, map, or cond node (not native-covered)`;
786
+ const cr = n;
787
+ if (cr.policy !== undefined && cr.policy !== "fail")
788
+ return `node '${n.id}' componentRef policy '${cr.policy}' (produced-aware output not yet native-covered)`;
789
+ if (cr.bindField !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
790
+ return `node '${n.id}' relationKind '${cr.relationKind ?? "point"}' not native-covered (bindField only supports single|connection)`;
791
+ if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
792
+ return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
793
+ for (const p of Object.keys(cr.ports))
794
+ if (portIsStatic(cr.ports[p]) === false)
795
+ return `node '${n.id}' port '${p}' is not statically resolvable`;
796
+ }
797
+ if (!outputIsNativeLowerable(comp))
798
+ return "output is not natively lowerable";
799
+ return null;
800
+ }
801
+ function isFullyTyped(comp) {
802
+ if (comp.outputType === undefined)
803
+ return false;
804
+ for (const n of comp.body)
805
+ if (n.outType === undefined)
806
+ return false;
807
+ return true;
808
+ }
809
+ function isNativeRaw(comp) {
810
+ return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
811
+ }
812
+ function assertTypedCoverage(comp) {
813
+ if (!isSequentialComponentRefRead(comp))
814
+ return;
815
+ if (isFullyTyped(comp))
816
+ return;
817
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust 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 rust-straightline-native (boxed result).`);
818
+ }
819
+ // ── typed input struct (InNR<Comp>) + NATIVE scalar port lowering ────────────────────────────
820
+ /** Rust type for an input port's declared type (scalar → concrete; array+elemType → Vec<ElemT>; else Value). */
821
+ function inputPortRustType(schema, plan) {
822
+ switch (schema?.type) {
823
+ case "string":
824
+ return "String";
825
+ case "int":
826
+ return "i64";
827
+ case "float":
828
+ return "f64";
829
+ case "bool":
830
+ return "bool";
831
+ case "array":
832
+ case "arr": {
833
+ // #108: an input ARRAY port with a declared elemType lowers to a native Vec<ElemT>.
834
+ const et = schema.elemType;
835
+ if (et !== undefined && plan !== undefined)
836
+ return `Vec<${renderTypeRef(deriveTypeRef(et, plan))}>`;
837
+ return "Value";
838
+ }
839
+ default:
840
+ return "Value";
841
+ }
842
+ }
843
+ /** The scalar kind an input port declares, or undefined if it is not a native scalar. */
844
+ function inputScalarKind(schema) {
845
+ switch (schema?.type) {
846
+ case "string":
847
+ return "String";
848
+ case "int":
849
+ return "i64";
850
+ case "float":
851
+ return "f64";
852
+ case "bool":
853
+ return "bool";
854
+ default:
855
+ return undefined;
856
+ }
857
+ }
858
+ /** emitInStruct — the concrete input struct declaration for a covered component (fields = inputPorts).
859
+ * Empty inputPorts => empty struct. Derives Default so a consumer/test can build it field-by-field. */
860
+ function emitInStruct(comp, plan) {
861
+ const name = inStructName(comp.name);
862
+ const ports = Object.keys(comp.inputPorts ?? {});
863
+ if (ports.length === 0) {
864
+ return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\n#[derive(Default)]\npub struct ${name};`;
865
+ }
866
+ const fields = ports
867
+ .map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p], plan)}, // ${JSON.stringify(p)}`)
868
+ .join("\n");
869
+ return `// ${name} — the CONCRETE input for '${comp.name}' (fields = inputPorts; typed, consumer-built —
870
+ // NO generic Value slice, NO per-field boxing crosses the covered read boundary).
871
+ #[derive(Default)]
872
+ pub struct ${name} {
873
+ ${fields}
874
+ }`;
875
+ }
876
+ /**
877
+ * emitNativeScalar — lower a static port expr to a NATIVE Rust expression of the concrete `want` scalar
878
+ * type, or null if it cannot be statically proven that type. A string literal is native for
879
+ * `want==="string"`, a bool literal for `want==="bool"`. A ref resolves via `resolveRef`. A concat lowers
880
+ * ONLY into `string` and only when EVERY part is a native string, emitting a single `format!("{}{}…", …)`.
881
+ */
882
+ function emitNativeScalar(node, want, resolveRef) {
883
+ if (staticArrElems(node) !== null)
884
+ return null;
885
+ const e = classifyExpr(node);
886
+ switch (e.kind) {
887
+ case "str":
888
+ return want === "String" ? `${rustStrLit(e.value)}.to_string()` : null;
889
+ case "bool":
890
+ return want === "bool" ? (e.value ? "true" : "false") : null;
891
+ case "null":
892
+ return null;
893
+ case "ref": {
894
+ const sc = resolveRef(e.path[0], e.path.slice(1), e.opt);
895
+ if (sc === null || sc.scalar !== want)
896
+ return null;
897
+ return sc.expr;
898
+ }
899
+ case "concat": {
900
+ if (want !== "String")
901
+ return null;
902
+ // a single-part concat is just the owned String of that part.
903
+ if (e.parts.length === 1)
904
+ return emitNativeScalar(reconstructR(e.parts[0]), "String", resolveRef);
905
+ // multi-part: fold into ONE format! (statically strings only — the concat's strings-only
906
+ // TYPE_MISMATCH can never fire, so dropping the check is sound). To stay clippy-clean
907
+ // (to_string_in_format_args), a string-literal part is baked into the format STRING itself
908
+ // (escaped) rather than passed as a `{}` arg; a ref part becomes a `{}` arg (Display on String).
909
+ let fmt = "";
910
+ const args = [];
911
+ for (const p of e.parts) {
912
+ const sub = classifyExpr(reconstructR(p));
913
+ if (sub.kind === "str") {
914
+ fmt += sub.value.replace(/[{}]/g, (c) => c + c); // escape { } for the format string
915
+ continue;
916
+ }
917
+ const arg = emitNativeScalar(reconstructR(p), "String", resolveRef);
918
+ if (arg === null)
919
+ return null;
920
+ fmt += "{}";
921
+ args.push(arg);
922
+ }
923
+ if (args.length === 0)
924
+ return `${rustStrLit(fmt)}.to_string()`;
925
+ return `format!(${rustStrLit(fmt)}, ${args.join(", ")})`;
926
+ }
927
+ default:
928
+ return null;
929
+ }
930
+ }
931
+ // bc#90 / runtime-free: the old `emitStaticR` / `inputLeafR` / `valueLeafR` port lowering (which built
932
+ // `Value` leaves via the primitives::* helpers and struct-field serializers) is REMOVED — every covered
933
+ // port now lowers to a FULLY NATIVE Rust value in emitPortInit (string / bool / static-string-array /
934
+ // number literal), and a genuinely-dynamic port fails closed. There is no boxed-Value fallback on the
935
+ // covered read plane.
936
+ /**
937
+ * emitPortInit — emit the port field build for one port as a FULLY NATIVE Rust expression (bc#90 /
938
+ * runtime-free). Every covered port lowers to a concrete Rust value assigned straight into the ports
939
+ * struct field — NO `Value`, NO fallback path, NO `match`:
940
+ * - a static string array (projection) → `vec!["a", "b", ...]` (change #2);
941
+ * - a bare int/float number literal (e.g. Query `limit: 20`) → `20i64` / `<n>f64`;
942
+ * - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
943
+ * A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered plane —
944
+ * a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
945
+ * ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
946
+ */
947
+ function emitPortInit(pn, expr, fieldRustType, resolveRef) {
948
+ const field = portFieldName(pn);
949
+ // change #2: a static string array (projection) → a native `vec![...]` of &'static str.
950
+ const strArr = staticStringArrElems(expr);
951
+ if (strArr !== null) {
952
+ if (fieldRustType !== "Vec<&'static str>") {
953
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90): port '${pn}' is a static string array but its field type is '${fieldRustType}' (expected Vec<&'static str>).`);
954
+ }
955
+ return `${field}: vec![${strArr.map((s) => rustStrLit(s)).join(", ")}]`;
956
+ }
957
+ // a bare number literal (e.g. Query `limit: 20`): the SSoT range check ran in numLiteralRustExpr —
958
+ // emit the concrete native i64/f64 (unwrap the Value::Int/Float box: it was only there for the old
959
+ // boxed path; the range check is what matters, and it already passed).
960
+ const num = numLiteralRustExpr(expr);
961
+ if (num !== null) {
962
+ if (fieldRustType === "i64")
963
+ return `${field}: ${expr}i64`;
964
+ if (fieldRustType === "f64")
965
+ return `${field}: ${expr}f64`;
966
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90): port '${pn}' is a number literal but its field type is '${fieldRustType}'.`);
967
+ }
968
+ // string / bool / scalar-input / concat → native scalar expression (no Value, no `match`).
969
+ const native = emitNativeScalar(expr, fieldRustType, resolveRef);
970
+ if (native !== null) {
971
+ return `${field}: ${native}`;
972
+ }
973
+ // FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would re-couple
974
+ // the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the component is not
975
+ // native-eligible and must regenerate on the boxed straight-line path.
976
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90 / runtime-free): port '${pn}' (${JSON.stringify(expr)}) does not lower to a native Rust value of type '${fieldRustType}'. The covered read plane admits NO boxed Value escape — regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
977
+ }
978
+ // ── the combined runner (STRUCT-returning; INLINE sequential; native ports in; concrete row out) ──
979
+ /** resolveRefAt for the top-level runner: a ref head is a PRIOR NODE (its typed scalar field) or an
980
+ * INPUT PORT (in_.<field>). A REQUIRED scalar resolves to an OWNED native Rust expression; opt / non-
981
+ * scalar / refOpt → null (fall back to the Value path). */
982
+ function runnerResolveRef(priorNodeAt, atPos, typedNodes, plan, inputPorts) {
983
+ return (head, restPath, opt) => {
984
+ if (opt)
985
+ return null;
986
+ if (priorNodeAt(head, atPos)) {
987
+ const baseRef = typedNodes.get(head);
988
+ let acc;
989
+ let expr;
990
+ try {
991
+ const r = typedFieldAccess(`${typedCell(head)}.borrow()`, baseRef, restPath, plan);
992
+ expr = r.expr;
993
+ acc = r.ref;
994
+ }
995
+ catch {
996
+ return null;
997
+ }
998
+ if (acc.kind !== "scalar" || acc.scalar === "null")
999
+ return null;
1000
+ // OWNED: a string field is cloned; int/float/bool are Copy (deref off the Ref).
1001
+ const owned = acc.scalar === "string" ? `(${expr}).clone()` : `*(${expr})`;
1002
+ return { expr: owned, scalar: rustScalar(acc.scalar) };
1003
+ }
1004
+ if (restPath.length !== 0)
1005
+ return null;
1006
+ const scalar = inputScalarKind(inputPorts?.[head]);
1007
+ if (scalar === undefined)
1008
+ return null;
1009
+ const field = `in_.${rustFieldName(head)}`;
1010
+ const owned = scalar === "String" ? `${field}.clone()` : field;
1011
+ return { expr: owned, scalar };
1012
+ };
1013
+ }
1014
+ function rustScalar(s) {
1015
+ return s === "string" ? "String" : s === "int" ? "i64" : s === "float" ? "f64" : "bool";
1016
+ }
1017
+ function emitNativeRawRunner(comp, plan, ap) {
1018
+ const ep = execPlan(comp);
1019
+ const order = ep.order;
1020
+ const hasConcurrency = ep.parallelStageOf.size > 0;
1021
+ const fn = sanitize(comp.name);
1022
+ const isAsync = ap.runnerIsAsync;
1023
+ // bc#97: the async runner awaits each async handler at its call site (derived per-node). A real-
1024
+ // concurrency stage (bc#87) dispatches handlers from scoped std threads — that mechanism cannot host
1025
+ // `.await` (a std thread is not an async task). Fail closed LOUDLY (rather than emit code that won't
1026
+ // compile) iff the runner is async AND has a concurrency stage. NOTE: with per-node granularity a
1027
+ // consumer CAN keep a concurrency-stage component sync (declare only its OTHER terminals async) — only
1028
+ // a component that both concurrency-stages AND awaits is rejected. Async + concurrent-stage (issuing
1029
+ // the stage's futures with join!) is a documented follow-up.
1030
+ if (isAsync && hasConcurrency) {
1031
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native async (bc#97): component '${comp.name}' has a bc#87 real-concurrency stage AND an async terminal handler, but a scoped-std-thread stage cannot host \`.await\`. Async + concurrent-stage (join! over the stage's futures) is a follow-up — declare this component's terminals sync, or split the concurrency stage.`);
1032
+ }
1033
+ const typedNodes = new Map();
1034
+ for (const n of comp.body)
1035
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1036
+ const idToIndex = new Map();
1037
+ comp.body.forEach((n, i) => idToIndex.set(n.id, i));
1038
+ const opsLiteral = comp.body.map((n) => {
1039
+ const cr = n;
1040
+ const pid = cr.parent;
1041
+ return { id: n.id, parent: pid !== undefined ? idToIndex.get(pid) ?? null : null, policy: cr.policy, bindField: cr.bindField };
1042
+ });
1043
+ const metas = analyzeSequentialOps(opsLiteral, order);
1044
+ const idToPos = new Map();
1045
+ order.forEach((opIdx, k) => idToPos.set(comp.body[opIdx].id, k));
1046
+ const priorNodeAt = (head, atPos) => {
1047
+ const p = idToPos.get(head);
1048
+ return p !== undefined && p < atPos;
1049
+ };
1050
+ const outRef = comp.outputType !== undefined ? deriveTypeRef(comp.outputType, plan) : undefined;
1051
+ const outTy = outRef ? renderTypeRef(outRef) : "Value";
1052
+ // Does any expression read an input param? If not, `in_` is unused — bind `_in_` to stay clippy-clean.
1053
+ // #108: this must recurse ALL expression operators (cond if/then/else, map when + ports, arith/compare/…),
1054
+ // not just ref/concat — a cond `if:{gt:[{add:[{ref:["x"]}…]}]}` reads `in_.x` through nested operators.
1055
+ const readsInput = (() => {
1056
+ // walk any Expression IR node; `bound` = names bound in this scope (a map $as) that are NOT input.
1057
+ const walk = (node, atPos, bound) => {
1058
+ if (node === null || typeof node === "boolean" || typeof node === "string" || typeof node === "number")
1059
+ return false;
1060
+ if (Array.isArray(node))
1061
+ return node.some((el) => walk(el, atPos, bound));
1062
+ const rec = node;
1063
+ const keys = Object.keys(rec);
1064
+ if (keys.length === 1 && (keys[0] === "ref" || keys[0] === "refOpt")) {
1065
+ const path = rec[keys[0]];
1066
+ const head = path[0];
1067
+ return !bound.has(head) && !priorNodeAt(head, atPos);
1068
+ }
1069
+ // any other single-key operator (obj/arr/concat/add/gt/cond/…): recurse into every child value.
1070
+ return keys.some((k) => walk(rec[k], atPos, bound));
1071
+ };
1072
+ return [...order.keys()].some((k) => {
1073
+ const nd = comp.body[order[k]];
1074
+ if ("map" in nd) {
1075
+ const m = nd.map;
1076
+ const bound = new Set([m.as]);
1077
+ // the map's OVER is evaluated in the outer scope (may read input); ports + when in element scope.
1078
+ if (walk(m.over, k, new Set()))
1079
+ return true;
1080
+ if (m.when !== undefined && walk(m.when, k, bound))
1081
+ return true;
1082
+ return Object.values(m.ports).some((pn) => walk(pn, k, bound));
1083
+ }
1084
+ if ("cond" in nd) {
1085
+ const c = nd.cond;
1086
+ return walk(c.if, k, new Set()) || walk(c.then, k, new Set()) || walk(c.else, k, new Set());
1087
+ }
1088
+ const cr = nd;
1089
+ return Object.values(cr.ports).some((pn) => walk(pn, k, new Set()));
1090
+ });
1091
+ })();
1092
+ const inParam = readsInput ? "in_" : "_in_";
1093
+ const lines = [];
1094
+ lines.push(`// run_native_raw_struct_${fn} — the STRUCT-RETURNING combined read (bc#77/#87/#94): the fully`);
1095
+ lines.push(`// de-plumbed CONCRETE path. Generic over the per-component CONCRETE ${handlerTraitName(comp.name)} trait,`);
1096
+ lines.push(`// whose node_* methods take the node's native ports struct and return the concrete per-node row`);
1097
+ lines.push(`// struct (typed fields = the outType). The runner builds each ports struct by direct native`);
1098
+ lines.push(`// construction (a simple string port lowers to a native Rust expr), dispatches the concrete`);
1099
+ lines.push(`// node_* method, and reads row.<field> DIRECTLY into the node's outType struct cell — no boxed`);
1100
+ lines.push(`// handler result, no generic enum crossing, no dispatch on a dynamic value on the covered plane. Node`);
1101
+ lines.push(`// results are typed struct cells; a relation child reads the parent's REAL struct result via`);
1102
+ lines.push(`// direct field access (child-present decision from the real parent value — relationSingle /`);
1103
+ lines.push(`// connection converge). A real-concurrency stage (bc#87) is static parallel orchestration —`);
1104
+ lines.push(`// scoped worker threads (bounded by the static plan.concurrency) call the concrete node_*`);
1105
+ lines.push(`// method; preflight + interpret are committed in ascending index order so the value / op`);
1106
+ lines.push(`// multiset / failure precedence byte-match run_behavior. The output is a typed struct/value`);
1107
+ lines.push(`// assembled by struct literal + field access — the consumer keeps it native.`);
1108
+ const bound = hasConcurrency ? `<H: ${handlerTraitName(comp.name)} + Sync>` : `<H: ${handlerTraitName(comp.name)}>`;
1109
+ // bc#97: async propagation (uniform) — if the terminal handlers are async, every handler call awaits,
1110
+ // so the runner is itself `async fn` and returns a future the (already-async) consumer awaits. Sync
1111
+ // default emits `pub fn` exactly as before (byte-identical).
1112
+ const fnKw = isAsync ? "pub async fn" : "pub fn";
1113
+ // #108: a component whose body is ALL cond nodes dispatches NO handler — bind `_handlers` (clippy-clean).
1114
+ const dispatchesHandler = comp.body.some((n) => "map" in n || "component" in n);
1115
+ lines.push(`${fnKw} run_native_raw_struct_${fn}${bound}(`);
1116
+ lines.push(` ${dispatchesHandler ? "handlers" : "_handlers"}: &H,`);
1117
+ lines.push(` ${inParam}: ${inStructName(comp.name)},`);
1118
+ lines.push(`) -> Result<${outTy}, BehaviorError> {`);
1119
+ for (const k of order.keys()) {
1120
+ const id = comp.body[order[k]].id;
1121
+ lines.push(` let ${typedCell(id)}: RefCell<${renderTypeRef(typedNodes.get(id))}> = RefCell::new(Default::default());`);
1122
+ lines.push(` let produced_${sanitize(id)} = std::cell::Cell::new(false);`);
1123
+ lines.push(` let _ = &produced_${sanitize(id)};`);
1124
+ }
1125
+ for (let k = 0; k < order.length; k++) {
1126
+ const i = order[k];
1127
+ const stage = ep.parallelStageOf.get(i);
1128
+ if (stage !== undefined && stage[0] === i) {
1129
+ lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt));
1130
+ k += stage.length - 1;
1131
+ continue;
1132
+ }
1133
+ if ("map" in comp.body[i]) {
1134
+ lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, ap.nodeIsAsync(comp.body[i].id)));
1135
+ continue;
1136
+ }
1137
+ // #108: a cond node — pure Expression (no handler). Compile via the runtime-free NativeExprCompiler.
1138
+ if ("cond" in comp.body[i]) {
1139
+ lines.push(emitCondArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt));
1140
+ continue;
1141
+ }
1142
+ const node = comp.body[i];
1143
+ const meta = metas[i];
1144
+ const s = sanitize(node.id);
1145
+ const structName = portsStructName(comp.name, node.id);
1146
+ const portNames = Object.keys(node.ports);
1147
+ const ref = typedNodes.get(node.id);
1148
+ lines.push(` // ── op '${node.id}' (${node.component}${node.relationKind ? ", relationKind:" + node.relationKind : ""}${node.parent ? ", parent:" + node.parent : ""}) ──`);
1149
+ let indent = " ";
1150
+ const closers = [];
1151
+ if (node.parent !== undefined && priorNodeAt(node.parent, k)) {
1152
+ lines.push(`${indent}if produced_${sanitize(node.parent)}.get() {`);
1153
+ closers.unshift(`${indent}}`);
1154
+ indent += " ";
1155
+ if (node.bindField !== undefined) {
1156
+ // bind (change #3 — typed produced-aware bound): read the parent struct field DIRECTLY (typed
1157
+ // access — no serialize). `bound_<s>` is a typed `Option<K>` — None => UNPRODUCED/skip, Some =>
1158
+ // produced (an empty string is a VALID produced value, `Some("")`). This is the EXACT `!= Null`
1159
+ // skip-gate ≡ run_behavior's preflightOp, now runtime-free (NO Value).
1160
+ const parentRef = typedNodes.get(node.parent);
1161
+ const { expr: bfExpr, ref: bfRef } = typedFieldAccess(`${typedCell(node.parent)}.borrow()`, parentRef, [node.bindField], plan);
1162
+ if (bfRef.kind === "opt") {
1163
+ // an opt bindField is ALREADY an Option<T>: None => skip, Some => produced. Clone it directly.
1164
+ lines.push(`${indent}let bound_${s}: ${boundRustType(node, typedNodes, plan)} = ${bfExpr}.clone();`);
1165
+ }
1166
+ else {
1167
+ // a required scalar bindField is always PRESENT (never skips on null): wrap in Some so the
1168
+ // produced Option is never None, preserving the "produced" semantics of the old guard.
1169
+ const owned = bfRef.kind === "scalar" && bfRef.scalar === "string" ? `(${bfExpr}).clone()` : `*(${bfExpr})`;
1170
+ lines.push(`${indent}let bound_${s}: ${boundRustType(node, typedNodes, plan)} = Some(${owned});`);
1171
+ }
1172
+ lines.push(`${indent}if bound_${s}.is_some() {`);
1173
+ closers.unshift(`${indent}}`);
1174
+ indent += " ";
1175
+ }
1176
+ }
1177
+ const resolveRef = runnerResolveRef(priorNodeAt, k, typedNodes, plan, comp.inputPorts);
1178
+ const inits = [];
1179
+ for (const pn of portNames) {
1180
+ const expr = node.ports[pn];
1181
+ const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`);
1182
+ inits.push(emitPortInit(pn, expr, ty, resolveRef));
1183
+ }
1184
+ const boundArg = node.bindField !== undefined ? `bound_${s}` : "None";
1185
+ // bc#97: derived per-node await — this point-read's future is awaited HERE (its result-consumption
1186
+ // point) iff THIS node's terminal handler is async. A sync handler in the same async runner is called
1187
+ // directly (no `.await`). The runner being `async fn` does not force every call site to await.
1188
+ const awaitSfx = ap.nodeIsAsync(node.id) ? ".await" : "";
1189
+ lines.push(`${indent}let ports_${s} = ${structName} { ${inits.join(", ")} };`);
1190
+ lines.push(`${indent}let row_${s} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
1191
+ lines.push(`${indent} Some(r) => r,`);
1192
+ lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(node.component)})),`);
1193
+ lines.push(`${indent}};`);
1194
+ if (meta.policy === "continue") {
1195
+ lines.push(`${indent}if !row_${s}.is_error {`);
1196
+ lines.push(`${indent} ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1197
+ lines.push(`${indent} produced_${s}.set(true);`);
1198
+ lines.push(`${indent}}`);
1199
+ }
1200
+ else {
1201
+ const label = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1202
+ lines.push(`${indent}if row_${s}.is_error {`);
1203
+ lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under '${label}: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
1204
+ lines.push(`${indent}}`);
1205
+ lines.push(`${indent}${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1206
+ lines.push(`${indent}produced_${s}.set(true);`);
1207
+ }
1208
+ for (const c of closers)
1209
+ lines.push(c);
1210
+ }
1211
+ // output: assemble a TYPED struct/value (struct literal + typed field access — ZERO Value::Obj).
1212
+ const skippable = new Set();
1213
+ const skippableKind = new Map();
1214
+ for (const k of order.keys()) {
1215
+ if (metas[order[k]].canSkip) {
1216
+ const bodyNode = comp.body[order[k]];
1217
+ skippable.add(bodyNode.id);
1218
+ const rk = "map" in bodyNode ? bodyNode.map.relationKind : bodyNode.relationKind;
1219
+ skippableKind.set(bodyNode.id, rk);
1220
+ }
1221
+ }
1222
+ const out = skippable.size > 0
1223
+ ? emitProducedAwareValue(comp.output, outRef, typedNodes, plan, skippable, skippableKind)
1224
+ : emitTypedValue(comp.output, outRef, typedNodes, plan);
1225
+ lines.push(` let __out = ${out.expr};`);
1226
+ lines.push(` Ok(__out)`);
1227
+ lines.push(`}`);
1228
+ return lines.join("\n");
1229
+ }
1230
+ /**
1231
+ * emitParallelStageArm (bc#87/#94, rust CONCRETE) — EXPLICIT static parallel orchestration for a real-
1232
+ * concurrency stage. Reproduces run_plan_parallel's deterministic protocol so the observed value / op
1233
+ * multiset / failure precedence byte-match run_behavior:
1234
+ * 1. PREFLIGHT (ascending): parent-produced gate + build each member's CONCRETE ports struct.
1235
+ * 2. DISPATCH (bounded scoped threads): share `&H` (the concrete trait; `H: Sync`), pull runnable
1236
+ * members from a shared atomic cursor in ascending order, call the concrete node_* method. Each
1237
+ * concrete row lands in a per-member Mutex slot (None = unknown component).
1238
+ * 3. COMMIT (ascending index): interpret policy + move the concrete row into the outType cell —
1239
+ * lowest-index failure wins.
1240
+ */
1241
+ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt) {
1242
+ const lines = [];
1243
+ const ids = stage.map((i) => comp.body[i].id);
1244
+ lines.push(` // ── bc#87 real-concurrency stage {${ids.map((x) => rustStrLit(x)).join(", ")}} — static parallel`);
1245
+ lines.push(` // orchestration (scoped threads, bound=${concurrency}); preflight+commit in ascending index order.`);
1246
+ lines.push(` {`);
1247
+ // 1) PREFLIGHT (ascending): build each member's CONCRETE ports struct into a stage-scoped Option local.
1248
+ for (const i of stage) {
1249
+ const node = comp.body[i];
1250
+ const s = sanitize(node.id);
1251
+ const structName = portsStructName(comp.name, node.id);
1252
+ const gated = node.parent !== undefined && priorNodeAt(node.parent, atPos);
1253
+ let ind = " ";
1254
+ const closers = [];
1255
+ if (gated) {
1256
+ lines.push(` let mut ports_${s}: Option<${structName}> = None;`);
1257
+ lines.push(`${ind}if produced_${sanitize(node.parent)}.get() {`);
1258
+ closers.unshift(`${ind}}`);
1259
+ ind += " ";
1260
+ }
1261
+ const resolveRef = runnerResolveRef(priorNodeAt, atPos, typedNodes, plan, comp.inputPorts);
1262
+ const inits = [];
1263
+ for (const pn of Object.keys(node.ports)) {
1264
+ const expr = node.ports[pn];
1265
+ const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`);
1266
+ inits.push(emitPortInit(pn, expr, ty, resolveRef));
1267
+ }
1268
+ if (gated) {
1269
+ lines.push(`${ind}ports_${s} = Some(${structName} { ${inits.join(", ")} });`);
1270
+ }
1271
+ else {
1272
+ lines.push(`${ind}let ports_${s}: Option<${structName}> = Some(${structName} { ${inits.join(", ")} });`);
1273
+ }
1274
+ for (const c of closers)
1275
+ lines.push(c);
1276
+ }
1277
+ // 2) DISPATCH (bounded scoped threads). Share `&H` (concrete trait; H: Sync). Each worker calls the
1278
+ // concrete node_* method for its member's CONCRETE ports and stores the concrete row in its slot.
1279
+ const nMembers = stage.length;
1280
+ const boundN = Math.min(concurrency, nMembers);
1281
+ const jobsVar = `jobs_${sanitize(comp.name)}`;
1282
+ for (const i of stage) {
1283
+ const s = sanitize(comp.body[i].id);
1284
+ const rowT = rawRowStructName(comp.name, comp.body[i].id);
1285
+ lines.push(` let slot_${s}: std::sync::Mutex<Option<Option<${rowT}>>> = std::sync::Mutex::new(None);`);
1286
+ }
1287
+ lines.push(` let mut ${jobsVar}: Vec<usize> = Vec::new();`);
1288
+ stage.forEach((i, slot) => {
1289
+ const s = sanitize(comp.body[i].id);
1290
+ lines.push(` if ports_${s}.is_some() { ${jobsVar}.push(${slot}); }`);
1291
+ });
1292
+ lines.push(` let cursor = std::sync::atomic::AtomicUsize::new(0);`);
1293
+ lines.push(` let workers = ${boundN}usize.min(${jobsVar}.len());`);
1294
+ lines.push(` std::thread::scope(|scope| {`);
1295
+ lines.push(` for _ in 0..workers {`);
1296
+ lines.push(` scope.spawn(|| loop {`);
1297
+ lines.push(` let j = cursor.fetch_add(1, std::sync::atomic::Ordering::SeqCst);`);
1298
+ lines.push(` if j >= ${jobsVar}.len() { break; }`);
1299
+ lines.push(` let slot = ${jobsVar}[j];`);
1300
+ lines.push(` match slot {`);
1301
+ stage.forEach((i, slot) => {
1302
+ const node = comp.body[i];
1303
+ const s = sanitize(node.id);
1304
+ lines.push(` ${slot} => {`);
1305
+ lines.push(` let ports = ports_${s}.as_ref().expect("runnable member has ports");`);
1306
+ lines.push(` let row = handlers.${nodeMethodName(node.id)}(ports, None);`);
1307
+ lines.push(` *slot_${s}.lock().unwrap() = Some(row);`);
1308
+ lines.push(` }`);
1309
+ });
1310
+ lines.push(` _ => unreachable!(),`);
1311
+ lines.push(` };`);
1312
+ lines.push(` });`);
1313
+ lines.push(` }`);
1314
+ lines.push(` });`);
1315
+ // 3) COMMIT (ascending index): interpret + move the concrete row into the outType cell.
1316
+ stage.forEach((i) => {
1317
+ const node = comp.body[i];
1318
+ const meta = metas[i];
1319
+ const s = sanitize(node.id);
1320
+ const ref = typedNodes.get(node.id);
1321
+ lines.push(` if ports_${s}.is_some() {`);
1322
+ lines.push(` let row_${s} = match slot_${s}.lock().unwrap().take() {`);
1323
+ lines.push(` Some(Some(r)) => r,`);
1324
+ lines.push(` _ => return Err(unknown_component(${rustStrLit(node.component)})),`);
1325
+ lines.push(` };`);
1326
+ if (meta.policy === "continue") {
1327
+ lines.push(` if !row_${s}.is_error {`);
1328
+ lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1329
+ lines.push(` produced_${s}.set(true);`);
1330
+ lines.push(` }`);
1331
+ }
1332
+ else {
1333
+ const label = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1334
+ lines.push(` if row_${s}.is_error {`);
1335
+ lines.push(` return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under '${label}: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
1336
+ lines.push(` }`);
1337
+ lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1338
+ lines.push(` produced_${s}.set(true);`);
1339
+ }
1340
+ lines.push(` }`);
1341
+ });
1342
+ lines.push(` }`);
1343
+ return lines.join("\n");
1344
+ }
1345
+ // bc#90 / runtime-free: the old `emitMapStaticR` map-element port lowering (a Value fallback) is REMOVED
1346
+ // — covered map element ports lower to a fully native Rust value in emitPortInit (the map resolveRef
1347
+ // reads the $as element / prior-node / input typed fields directly), with a fail-closed on a dynamic port.
1348
+ /**
1349
+ * emitMapArm — the struct-native exec of a covered map node (rust CONCRETE). Three shapes:
1350
+ * - #86 pt2: BATCHED map...into — dispatch once (concrete batch row), augmented-element materializer.
1351
+ * - #93 shape 2: NON-batched map...into — dispatch ONCE PER ELEMENT (N physical requests), into per element.
1352
+ * - #93 shape 3: BATCHED map NO into — dispatch once, collect each element from the concrete element row.
1353
+ */
1354
+ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt,
1355
+ // bc#97: whether THIS map node's terminal handler is async (derived per-node) — awaits the batched /
1356
+ // per-element handler call at its consumption point iff true. Sync map handlers are called directly.
1357
+ isAsync = false) {
1358
+ const m = node.map;
1359
+ const awaitSfx = isAsync ? ".await" : "";
1360
+ const batched = m.batched === true;
1361
+ const hasInto = m.into !== undefined;
1362
+ const hasGuard = m.when !== undefined;
1363
+ const s = sanitize(node.id);
1364
+ const structName = portsStructName(comp.name, node.id);
1365
+ const batch = `${structName}Batch`;
1366
+ // #108: over element type + the OWNED over-Vec expression (prior-node arr OR input array w/ elemType).
1367
+ const { elemRef: overElemRef, overExpr } = mapOverElemInfo(comp, node, typedNodes, plan);
1368
+ const elemMat = mapElemMaterializerName(node.id);
1369
+ const priorHere = (head) => priorNodeAt(head, atPos);
1370
+ const asName = m.as;
1371
+ const portNames = Object.keys(m.ports);
1372
+ const elemArrRef = typedNodes.get(node.id);
1373
+ const elemName = renderTypeRef(elemArrRef.elem);
1374
+ const resolveRef = (head, restPath, opt) => {
1375
+ if (opt)
1376
+ return null;
1377
+ let baseExpr;
1378
+ let baseRef;
1379
+ if (head === asName) {
1380
+ baseExpr = `oel_${s}`;
1381
+ baseRef = overElemRef;
1382
+ }
1383
+ else if (priorHere(head)) {
1384
+ baseExpr = `${typedCell(head)}.borrow()`;
1385
+ baseRef = typedNodes.get(head);
1386
+ }
1387
+ else {
1388
+ if (restPath.length !== 0)
1389
+ return null;
1390
+ const scalar = inputScalarKind(comp.inputPorts?.[head]);
1391
+ if (scalar === undefined)
1392
+ return null;
1393
+ const field = `in_.${rustFieldName(head)}`;
1394
+ return { expr: scalar === "String" ? `${field}.clone()` : field, scalar };
1395
+ }
1396
+ let acc;
1397
+ let expr;
1398
+ try {
1399
+ const r = typedFieldAccess(baseExpr, baseRef, restPath, plan);
1400
+ expr = r.expr;
1401
+ acc = r.ref;
1402
+ }
1403
+ catch {
1404
+ return null;
1405
+ }
1406
+ if (acc.kind !== "scalar" || acc.scalar === "null")
1407
+ return null;
1408
+ // OWNED value. When restPath is EMPTY, `expr` is a bare REFERENCE (`oel_${s}` = &Elem-scalar, or
1409
+ // `cell.borrow()` = Ref<scalar>) — deref with `*` (Copy) / `.clone()` via deref (String). When a
1410
+ // `.field` was appended, `expr` is a value PLACE (auto-deref) — read it directly (`.clone()` String).
1411
+ const isBareRef = restPath.length === 0;
1412
+ let owned;
1413
+ if (acc.scalar === "string") {
1414
+ owned = isBareRef ? `(*${expr}).clone()` : `${expr}.clone()`;
1415
+ }
1416
+ else {
1417
+ owned = isBareRef ? `*${expr}` : expr;
1418
+ }
1419
+ return { expr: owned, scalar: rustScalar(acc.scalar) };
1420
+ };
1421
+ // build the per-element CONCRETE native ports struct. `il` = indent inside the element loop.
1422
+ const buildPorts = (il) => {
1423
+ const out = [];
1424
+ const inits = [];
1425
+ const asBinding = { name: asName, ref: overElemRef, plan };
1426
+ for (const pn of portNames) {
1427
+ const ty = portFieldRustType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding);
1428
+ inits.push(emitPortInit(pn, m.ports[pn], ty, resolveRef));
1429
+ }
1430
+ out.push(`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`);
1431
+ return out;
1432
+ };
1433
+ // #108 guard: compile `when` to a native bool over an element-scoped resolveHead ($as element value,
1434
+ // prior node, input port). On skip: `continue` (element excluded — matches run_behavior's keep-gate).
1435
+ const emitGuard = (il) => {
1436
+ if (!hasGuard)
1437
+ return [];
1438
+ const resolveHead = (head) => {
1439
+ if (head === asName)
1440
+ return { expr: `oel_${s}.clone()`, ref: overElemRef };
1441
+ if (priorHere(head))
1442
+ return { expr: `${typedCell(head)}.borrow().clone()`, ref: typedNodes.get(head) };
1443
+ return rustInputHeadRef(head, comp, plan);
1444
+ };
1445
+ const g = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compileBool(m.when);
1446
+ const out = [];
1447
+ for (const st of g.stmts)
1448
+ out.push(`${il}${st}`);
1449
+ // bind the keep flag to a var so clippy's nonminimal_bool doesn't rewrite `!(a > b)` (the `when`
1450
+ // expr must stay verbatim = run_behavior's `{cond:[when,true,false]}` strict-bool keep-gate).
1451
+ out.push(`${il}let __keep = ${rustStripOuterParens(g.expr)};`);
1452
+ out.push(`${il}if !__keep { continue; }`);
1453
+ return out;
1454
+ };
1455
+ const lines = [];
1456
+ const closers = [];
1457
+ lines.push(` // ── map '${node.id}' (${m.component}, ${batched ? "batched" : "per-element"}, into:${JSON.stringify(m.into ?? null)}${hasGuard ? ", guarded" : ""}${m.relationKind ? ", relationKind:" + m.relationKind : ""}${m.parent ? ", parent:" + m.parent : ""}) ──`);
1458
+ let indent = " ";
1459
+ if (m.parent !== undefined && priorNodeAt(m.parent, atPos)) {
1460
+ lines.push(`${indent}if produced_${sanitize(m.parent)}.get() {`);
1461
+ closers.unshift(`${indent}}`);
1462
+ indent += " ";
1463
+ }
1464
+ lines.push(`${indent}let over_${s} = ${overExpr};`);
1465
+ lines.push(`${indent}let mut built_${s}: Vec<${elemName}> = Vec::with_capacity(over_${s}.len());`);
1466
+ if (batched) {
1467
+ // build kept-element ports (+ kept over elements for into). Batched into is never guarded (rejected).
1468
+ lines.push(`${indent}let mut items_${s}: Vec<${structName}> = Vec::with_capacity(over_${s}.len());`);
1469
+ if (hasInto)
1470
+ lines.push(`${indent}let mut kept_${s}: Vec<${renderTypeRef(overElemRef)}> = Vec::with_capacity(over_${s}.len());`);
1471
+ lines.push(`${indent}for oel_${s} in over_${s}.iter() {`);
1472
+ for (const l of emitGuard(indent + " "))
1473
+ lines.push(l);
1474
+ for (const l of buildPorts(indent + " "))
1475
+ lines.push(l);
1476
+ lines.push(`${indent} items_${s}.push(ep_${s});`);
1477
+ if (hasInto)
1478
+ lines.push(`${indent} kept_${s}.push(oel_${s}.clone());`);
1479
+ lines.push(`${indent}}`);
1480
+ lines.push(`${indent}if !items_${s}.is_empty() {`);
1481
+ lines.push(`${indent} let want_${s} = items_${s}.len();`);
1482
+ lines.push(`${indent} let bp_${s} = ${batch} { items: items_${s} };`);
1483
+ lines.push(`${indent} let row_${s} = match handlers.${nodeMethodName(node.id)}(&bp_${s}, None)${awaitSfx} {`);
1484
+ lines.push(`${indent} Some(r) => r,`);
1485
+ lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(m.component)})),`);
1486
+ lines.push(`${indent} };`);
1487
+ lines.push(`${indent} if row_${s}.is_error {`);
1488
+ lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
1489
+ lines.push(`${indent} }`);
1490
+ lines.push(`${indent} if row_${s}.rows.len() != want_${s} {`);
1491
+ lines.push(`${indent} return Err(BehaviorError::new("MAP_BATCH_RESULT_MISMATCH", format!("map '{}': batched handler must return a list aligned to items (want {})", ${rustStrLit(node.id)}, want_${s})));`);
1492
+ lines.push(`${indent} }`);
1493
+ if (hasInto) {
1494
+ // zip the aligned batch rows onto each KEPT over element (into materializer).
1495
+ lines.push(`${indent} let mut rows_iter_${s} = row_${s}.rows.into_iter();`);
1496
+ lines.push(`${indent} for over_el_${s} in kept_${s}.iter() {`);
1497
+ lines.push(`${indent} let er_${s} = rows_iter_${s}.next().expect("aligned batch row");`);
1498
+ lines.push(`${indent} built_${s}.push(${elemMat}(over_el_${s}, er_${s}));`);
1499
+ lines.push(`${indent} }`);
1500
+ }
1501
+ else {
1502
+ // no-into: collect each element DIRECTLY from the aligned per-element row (result = kept rows).
1503
+ lines.push(`${indent} for er_${s} in row_${s}.rows.into_iter() {`);
1504
+ lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
1505
+ lines.push(`${indent} built_${s}.push(el_${s});`);
1506
+ lines.push(`${indent} }`);
1507
+ }
1508
+ lines.push(`${indent}}`);
1509
+ }
1510
+ else {
1511
+ // #93 shape 2 / #108: NON-batched map — dispatch ONCE PER KEPT ELEMENT.
1512
+ lines.push(`${indent}for oel_${s} in over_${s}.iter() {`);
1513
+ for (const l of emitGuard(indent + " "))
1514
+ lines.push(l);
1515
+ for (const l of buildPorts(indent + " "))
1516
+ lines.push(l);
1517
+ lines.push(`${indent} let er_${s} = match handlers.${nodeMethodName(node.id)}(&ep_${s}, None)${awaitSfx} {`);
1518
+ lines.push(`${indent} Some(r) => r,`);
1519
+ lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(m.component)})),`);
1520
+ lines.push(`${indent} };`);
1521
+ lines.push(`${indent} if er_${s}.is_error {`);
1522
+ lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(node.id)}, er_${s}.err)));`);
1523
+ lines.push(`${indent} }`);
1524
+ if (hasInto) {
1525
+ lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s}, er_${s}));`);
1526
+ }
1527
+ else {
1528
+ lines.push(`${indent} let _ = &oel_${s};`);
1529
+ lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
1530
+ lines.push(`${indent} built_${s}.push(el_${s});`);
1531
+ }
1532
+ lines.push(`${indent}}`);
1533
+ }
1534
+ lines.push(`${indent}*${typedCell(node.id)}.borrow_mut() = built_${s};`);
1535
+ lines.push(`${indent}produced_${sanitize(node.id)}.set(true);`);
1536
+ for (const c of closers)
1537
+ lines.push(c);
1538
+ return lines.join("\n");
1539
+ }
1540
+ /** #108: resolve an INPUT port head for a native-expr ref (rust). Scalar → `in_.<field>` (owned: String
1541
+ * cloned, Copy as-is); array WITH elemType → `in_.<field>.clone()` of Vec<ElemT> (for `len`); else null. */
1542
+ function rustInputHeadRef(head, comp, plan) {
1543
+ const schema = comp.inputPorts?.[head];
1544
+ if (schema === undefined)
1545
+ return null;
1546
+ const sc = inputScalarKind(schema);
1547
+ if (sc !== undefined) {
1548
+ const scalarRef = { kind: "scalar", scalar: sc === "i64" ? "int" : sc === "f64" ? "float" : sc === "String" ? "string" : "bool" };
1549
+ const field = `in_.${rustFieldName(head)}`;
1550
+ return { expr: sc === "String" ? `${field}.clone()` : field, ref: scalarRef };
1551
+ }
1552
+ const et = schema.elemType;
1553
+ if ((schema.type === "array" || schema.type === "arr") && et !== undefined) {
1554
+ // no clone: the only array use inside a cond/guard expr is `len`, which BORROWS (`&in_.xs`).
1555
+ return { expr: `in_.${rustFieldName(head)}`, ref: { kind: "arr", elem: deriveTypeRef(et, plan) } };
1556
+ }
1557
+ return null;
1558
+ }
1559
+ /**
1560
+ * emitCondArm — the struct-native exec of a covered cond node (#108, rust). Mirrors run_behavior's cond
1561
+ * lower: compile `if` to a native bool, materialize the TAKEN branch to the node's outType (only the
1562
+ * taken branch evaluated), assign the node's typed cell + set produced. NO handler dispatch. Compiles
1563
+ * via the runtime-free NativeExprCompiler (byte-equal to `evaluate`); an uncovered expr fails closed.
1564
+ */
1565
+ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
1566
+ const c = node.cond;
1567
+ const s = sanitize(node.id);
1568
+ const nodeRef = typedNodes.get(node.id);
1569
+ const priorHere = (head) => priorNodeAt(head, atPos);
1570
+ const resolveHead = (head) => {
1571
+ if (priorHere(head))
1572
+ return { expr: `${typedCell(head)}.borrow().clone()`, ref: typedNodes.get(head) };
1573
+ return rustInputHeadRef(head, comp, plan);
1574
+ };
1575
+ const compiler = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan));
1576
+ const resolveDecl = (name) => findDecl(plan, name).fields;
1577
+ const cond = compiler.compileBool(c.if);
1578
+ const thenC = compiler.compileTo(c.then, nodeRef, resolveDecl);
1579
+ const elseC = compiler.compileTo(c.else, nodeRef, resolveDecl);
1580
+ const lines = [];
1581
+ lines.push(` // ── cond '${node.id}'${c.parent ? " (parent:" + c.parent + ")" : ""} — pure Expression (no handler) ──`);
1582
+ let indent = " ";
1583
+ const closers = [];
1584
+ if (c.parent !== undefined && priorNodeAt(c.parent, atPos)) {
1585
+ lines.push(`${indent}if produced_${sanitize(c.parent)}.get() {`);
1586
+ closers.unshift(`${indent}}`);
1587
+ indent += " ";
1588
+ }
1589
+ for (const st of cond.stmts)
1590
+ lines.push(`${indent}${st}`);
1591
+ lines.push(`${indent}if ${rustStripOuterParens(cond.expr)} {`);
1592
+ for (const st of thenC.stmts)
1593
+ lines.push(`${indent} ${st}`);
1594
+ lines.push(`${indent} *${typedCell(node.id)}.borrow_mut() = ${thenC.expr};`);
1595
+ lines.push(`${indent}} else {`);
1596
+ for (const st of elseC.stmts)
1597
+ lines.push(`${indent} ${st}`);
1598
+ lines.push(`${indent} *${typedCell(node.id)}.borrow_mut() = ${elseC.expr};`);
1599
+ lines.push(`${indent}}`);
1600
+ lines.push(`${indent}produced_${s}.set(true);`);
1601
+ for (const cl of closers)
1602
+ lines.push(cl);
1603
+ return lines.join("\n");
1604
+ }
1605
+ /** emitElemMove — move a concrete element row (RawElemNR, error already handled) into a fresh `let` local
1606
+ * of the element outType. Named → struct literal from moved fields; scalar/arr/opt → .val. */
1607
+ function emitElemMove(letBind, rowExpr, ref, plan) {
1608
+ if (ref.kind === "named") {
1609
+ const decl = findDecl(plan, ref.name);
1610
+ const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: ${rowExpr}.${rustFieldName(f.name)}`).join(", ");
1611
+ return `${letBind} = ${ref.name} { ${inits} };`;
1612
+ }
1613
+ return `${letBind} = ${rowExpr}.val;`;
1614
+ }
1615
+ /**
1616
+ * emitProducedAwareValue — the #86 part 1 produced-aware output lowering (rust twin of the go one).
1617
+ */
1618
+ function emitProducedAwareValue(node, expected, typedNodes, plan, skippable, skippableKind) {
1619
+ const e = classifyExpr(node);
1620
+ if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
1621
+ const head = e.path[0];
1622
+ const baseRef = typedNodes.get(head);
1623
+ if (skippable.has(head)) {
1624
+ const base = `${typedCell(head)}.borrow().clone()`;
1625
+ const { expr, ref } = typedFieldAccess(base, baseRef, e.path.slice(1), plan);
1626
+ const producedExpr = valueLeafProduced(expr, ref, expected);
1627
+ const isConn = skippableKind.get(head) === "connection" && e.path.length === 1;
1628
+ let unproduced;
1629
+ if (isConn) {
1630
+ const connTy = expected && expected.kind === "opt" ? renderTypeRef(expected.inner) : renderTypeRef(expected ?? ref);
1631
+ unproduced = expected && expected.kind === "opt" ? `Some(${connTy}::default())` : `${connTy}::default()`;
1632
+ }
1633
+ else {
1634
+ unproduced = expected && expected.kind === "opt" ? "None" : "Default::default()";
1635
+ }
1636
+ return {
1637
+ expr: `if produced_${sanitize(head)}.get() { ${producedExpr} } else { ${unproduced} }`,
1638
+ ref: expected ?? ref,
1639
+ };
1640
+ }
1641
+ const isBareCopyScalar = e.path.length === 1 && baseRef.kind === "scalar" && (baseRef.scalar === "int" || baseRef.scalar === "float" || baseRef.scalar === "bool");
1642
+ const base = isBareCopyScalar ? `*${typedCell(head)}.borrow()` : `${typedCell(head)}.borrow().clone()`;
1643
+ const { expr, ref } = typedFieldAccess(base, baseRef, e.path.slice(1), plan);
1644
+ return { expr, ref };
1645
+ }
1646
+ if (node !== null && typeof node === "object" && !Array.isArray(node)) {
1647
+ const keys = Object.keys(node);
1648
+ if (keys.length === 1 && keys[0] === "obj" && expected && expected.kind === "named") {
1649
+ const decl = findDecl(plan, expected.name);
1650
+ const fields = node.obj;
1651
+ const inits = decl.fields.map((f) => {
1652
+ const sub = emitProducedAwareValue(fields[f.name], f.type, typedNodes, plan, skippable, skippableKind);
1653
+ return `${rustFieldName(f.name)}: ${sub.expr}`;
1654
+ });
1655
+ return { expr: `${expected.name} { ${inits.join(", ")} }`, ref: expected };
1656
+ }
1657
+ }
1658
+ return emitTypedValue(node, expected, typedNodes, plan);
1659
+ }
1660
+ function valueLeafProduced(expr, ref, expected) {
1661
+ if (expected && expected.kind === "opt") {
1662
+ if (ref.kind === "opt")
1663
+ return expr;
1664
+ return `Some(${expr})`;
1665
+ }
1666
+ return expr;
1667
+ }
1668
+ // bc#90 / runtime-free: the old `valueLeafInner` (which serialized a bindField to a boxed `Value` for the
1669
+ // binding check) is REMOVED — the bound value is now a typed `Option<K>` built by direct field access
1670
+ // (Some(field.clone()) / the opt field's Option), and the produced gate is `Option::is_some()`.
1671
+ // ── struct dispatch surface (STRUCT-only — no Value bind, no serializer) ─────────────────
1672
+ function emitStructSurface(native) {
1673
+ const names = native.map((c) => rustStrLit(c.name));
1674
+ return `// COMPONENT_NAMES_NATIVE_RAW — covered reads exposed on the combined struct-native path. Each is
1675
+ // driven via run_native_raw_struct_<comp>(handlers, in_) -> Result<T, BehaviorError>: a STRUCT return
1676
+ // (the consumer builds the CONCRETE InNR_<comp> input + implements the CONCRETE HandlerNR<comp> trait).
1677
+ // See INTEGRATION.md §6.
1678
+ pub const COMPONENT_NAMES_NATIVE_RAW: [&str; ${names.length}] = [${names.join(", ")}];`;
1679
+ }
1680
+ // ── unknown_component helper (self-contained; the module carries no straight-line body) ──
1681
+ const UNKNOWN_COMPONENT_HELPER = `#[allow(dead_code)]
1682
+ fn unknown_component(component: &str) -> BehaviorError {
1683
+ BehaviorError::new("UNKNOWN_COMPONENT", format!("component '{component}' has no handler (fail-closed)"))
1684
+ }`;
1685
+ // ── module assembly ──────────────────────────────────────────────────────────────────
1686
+ function emit(ctx) {
1687
+ // bc#97 (increment 2): the SSoT language-agnostic propagation pass derives, PER COMPONENT + PER NODE,
1688
+ // which handler-call boundaries must be awaited (bottom-up over the declared async terminal handlers).
1689
+ // rust CONSUMES this annotation (async fn + .await where derived); go ignores it. per-node granularity
1690
+ // is a strict generalization of increment-1's uniform ioModel:'async' (= every terminal async).
1691
+ const asyncPlans = buildAsyncPlans(ctx.ir, ctx.ioModel);
1692
+ for (const c of ctx.ir.components)
1693
+ assertTypedCoverage(c);
1694
+ const native = ctx.ir.components.filter(isNativeRaw);
1695
+ const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
1696
+ // FAIL CLOSED (bc#86/#89): the --typed-native (native codegen) surface is native-only. Previously a
1697
+ // non-covered component was SILENTLY DROPPED (emitted only as a comment note), concealing that it
1698
+ // never got native codegen. We now throw a LOUD GeneratorFailure naming every uncovered component +
1699
+ // its specific reject reason. A fully-covered module is unchanged.
1700
+ if (nonNative.length > 0) {
1701
+ const rejects = nonNative
1702
+ .map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
1703
+ .join("\n");
1704
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (native codegen) surface: ${nonNative.length} component(s) are NOT a covered native read:\n${rejects}\n\n` +
1705
+ `The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
1706
+ `box uncovered shapes. Implement native coverage for this shape, fix the spec if the shape is ` +
1707
+ `genuinely runtime-dynamic, or generate this component on the --ir dynamic interpreter surface.`);
1708
+ }
1709
+ if (native.length === 0) {
1710
+ // an empty native surface (no covered read) carries no runner and no failure — nothing references
1711
+ // bc-runtime, so it stays runtime-free (bc#90) with only the component-names const.
1712
+ return `#![allow(dead_code, unused_imports)]
1713
+ ${emitStructSurface([])}
1714
+ `;
1715
+ }
1716
+ const plan = buildTypePlan(ctx.ir);
1717
+ // The typed-native module is a de-boxed CONSUMER artifact: the covered runner RETURNS these outType
1718
+ // structs and the consumer reads their fields natively. So the decls (and their FIELDS) are pub — the
1719
+ // consumer keeps the model native (mirrors Go's exported outType struct fields on the covered path).
1720
+ const decls = emitTypeDecls(plan)
1721
+ .replace(/\nstruct /g, "\npub struct ")
1722
+ .replace(/^struct /gm, "pub struct ")
1723
+ .replace(/^( {4})([a-z_][A-Za-z0-9_]*: )/gm, "$1pub $2")
1724
+ .replace(/^( {4})(r#[a-z_][A-Za-z0-9_]*: )/gm, "$1pub $2");
1725
+ const defaults = emitDefaults(plan);
1726
+ // #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler).
1727
+ rustExprUsed.helpers = false;
1728
+ const structs = [];
1729
+ const rowStructs = [];
1730
+ const handlerTraits = [];
1731
+ const inStructs = [];
1732
+ const elemMaterializers = [];
1733
+ for (const c of native) {
1734
+ const typedNodes = new Map();
1735
+ // build the full typed-node map FIRST so a map's over-element resolution can see every node.
1736
+ for (const n of c.body)
1737
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1738
+ for (const n of c.body) {
1739
+ if ("map" in n)
1740
+ structs.push(emitMapPortsStructs(c, n, typedNodes, plan));
1741
+ else if ("cond" in n) {
1742
+ // #108: a cond node has NO ports struct (pure Expression — no handler dispatch).
1743
+ }
1744
+ else
1745
+ structs.push(emitPortsStruct(c, n));
1746
+ }
1747
+ rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
1748
+ handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
1749
+ inStructs.push(emitInStruct(c, plan));
1750
+ const mm = emitMapElemMaterializers(c, typedNodes, plan);
1751
+ if (mm)
1752
+ elemMaterializers.push(mm);
1753
+ }
1754
+ const runners = native.map((c) => emitNativeRawRunner(c, plan, asyncPlans.get(c.name))).join("\n\n");
1755
+ const structSurface = emitStructSurface(native);
1756
+ // (nonNative.length === 0 is guaranteed here — the fail-closed guard above threw otherwise.)
1757
+ const banner = `#![allow(dead_code, unused_imports, non_snake_case)]
1758
+ // GENERATED by behavior-contracts (bc#90, rust-typed-native — the RUNTIME-FREE read de-box; STRUCT-ONLY,
1759
+ // FULLY-NATIVE). A covered read is a strictly-sequential typed componentRef chain (point reads +
1760
+ // relationKind:single children), emitted ONLY as a STRUCT-returning runner GENERIC over a per-component
1761
+ // CONCRETE handler trait (HandlerNR<comp>): native ports struct IN (direct construction — every port is a
1762
+ // CONCRETE Rust value: String / bool / Vec<&str> projection / i64 limit — NO boxed Value, NO by-name
1763
+ // accessor); the handler takes a typed produced-aware 'bound' Option and returns the node's CONCRETE row
1764
+ // struct (typed fields = the projected outType), and the runner reads row.<field> DIRECTLY into the node's
1765
+ // outType struct. The covered plane carries NO bc-runtime reference at all — failures use the LOCAL
1766
+ // BehaviorError (same codes, byte-equal to run_behavior). INLINE sequential (no plan driver) so a relation
1767
+ // child reads the parent's REAL struct result via direct field access and the child-present decision is
1768
+ // made from the real parent value — relationSingle / connection converge with run_behavior (fixes
1769
+ // #323/#74). The exposed API is run_native_raw_struct_<comp>(handlers, in_) over the CONCRETE InNR_<comp>
1770
+ // input struct + the CONCRETE HandlerNR<comp> trait (the consumer builds/implements both, keeping the
1771
+ // model native). This module is FULLY NATIVE: a naive grep for boxing primitives OR for the bc-runtime
1772
+ // crate finds nothing, by design (bc#90 / runtime-free).
1773
+ use std::cell::RefCell;`;
1774
+ const sections = [
1775
+ banner,
1776
+ `// Local concrete failure type (runtime-free) — a covered runner returns Result<T, BehaviorError> over\n// THIS local type instead of a bc-runtime failure, so the fully-covered module imports ZERO bc runtime.\n${emitBehaviorErrorType()}`,
1777
+ decls || undefined,
1778
+ defaults || undefined,
1779
+ UNKNOWN_COMPONENT_HELPER,
1780
+ rustExprUsed.helpers
1781
+ ? `// #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${emitExprHelpersRust()}`
1782
+ : undefined,
1783
+ structs.some((r) => r.trim().length > 0)
1784
+ ? `// Native ports structs (one per componentRef node; typed per the static port type — CONCRETE).\n${structs.filter((r) => r.trim().length > 0).join("\n\n")}`
1785
+ : undefined,
1786
+ rowStructs.some((r) => r.trim().length > 0)
1787
+ ? `// CONCRETE per-node row structs (typed fields = the node's outType; + error signal).\n${rowStructs.filter((r) => r.trim().length > 0).join("\n\n")}`
1788
+ : undefined,
1789
+ `// CONCRETE per-component input structs (fields = inputPorts).\n${inStructs.join("\n\n")}`,
1790
+ `// CONCRETE per-component handler traits (one node_* method per node — native ports IN, row OUT).\n${handlerTraits.join("\n\n")}`,
1791
+ elemMaterializers.length
1792
+ ? `// map augmented-element copiers (pure struct field assignment — no dynamic value read).\n${elemMaterializers.join("\n\n")}`
1793
+ : undefined,
1794
+ `// Combined read runners (STRUCT-returning — the fully de-plumbed CONCRETE path).\n${runners}`,
1795
+ structSurface,
1796
+ ];
1797
+ return `${sections.filter((x) => !!x).join("\n\n")}\n`;
1798
+ }
1799
+ /**
1800
+ * rustTypedNativeObserve — the TEST-ONLY observe companion (a submodule string the test harness writes
1801
+ * alongside the covered module). It carries the typed->Value serializers (ser_*) + a CONCRETE scripted
1802
+ * handler (ScriptedNativeRaw) that implements every HandlerNR<comp> trait by decoding a scripted Value
1803
+ * into the concrete per-node row, + an observe_native_raw dispatch that decodes the generic input into the
1804
+ * concrete InNR_<comp>, drives the module-private struct runner, and serializes its result to a canonical
1805
+ * Value for the equivalence pin. Lives OUTSIDE the covered module so the module stays literal whole-file
1806
+ * zero (the ser_* / decode helpers are test glue, never on the consumer's read hot path).
1807
+ */
1808
+ export function rustTypedNativeObserve(ir, ioModel = "sync") {
1809
+ const components = ir.components;
1810
+ const native = components.filter(isNativeRaw);
1811
+ const plan = buildTypePlan(ir);
1812
+ const needSync = native.some((c) => concurrencyPlan(c) !== null);
1813
+ const serializers = emitSerializers(plan);
1814
+ const marshallers = emitMarshallers(plan);
1815
+ // bc#97: the observe glue must MATCH the covered module's per-node async-ness — the scripted handler's
1816
+ // node_* methods are `async fn` exactly where the trait's are, and observe_native_raw awaits an async
1817
+ // runner. Built from the SAME SSoT propagation pass the covered emitter used, so glue never drifts.
1818
+ const asyncPlans = buildAsyncPlans(ir, ioModel);
1819
+ const anyAsyncRunner = native.some((c) => asyncPlans.get(c.name).runnerIsAsync);
1820
+ // ── per-component input decoders: generic &[(String,Value)] -> concrete InNR_<comp> (test glue). ──
1821
+ const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
1822
+ // ── concrete scripted handler: implements every HandlerNR<comp> trait by decoding a scripted Value
1823
+ // into the concrete per-node row (the generic scripted-source queue meets the concrete seam). ──
1824
+ const decodeFns = [];
1825
+ const emittedDecode = new Set();
1826
+ const traitImpls = [];
1827
+ for (const c of native) {
1828
+ const ap = asyncPlans.get(c.name);
1829
+ // the scripted impl's node_* is `async fn` iff the trait's is (same per-node derivation).
1830
+ const fnKwFor = (nodeId) => (ap.nodeIsAsync(nodeId) ? "async fn" : "fn");
1831
+ const typedNodes = new Map();
1832
+ for (const n of c.body)
1833
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1834
+ const methods = [];
1835
+ for (const n of c.body) {
1836
+ if ("cond" in n)
1837
+ continue; // #108: a cond node has no handler method (pure Expression) — no scripted impl.
1838
+ const ref = typedNodes.get(n.id);
1839
+ const fnKw = fnKwFor(n.id);
1840
+ // bc#90 / runtime-free: the scripted impl's `_bound` param mirrors the trait's typed produced-aware
1841
+ // Option<K> (NOT a boxed Value) — computed per node so the impl signature matches the trait.
1842
+ const bt = boundRustType(n, typedNodes, plan);
1843
+ if ("map" in n) {
1844
+ const m = n.map;
1845
+ const batched = m.batched === true;
1846
+ const arrRef = ref;
1847
+ const elemRowRef = mapElemRowRef(n, arrRef, plan);
1848
+ const elemT = rawElemStructName(c.name, n.id);
1849
+ const batchT = rawRowStructName(c.name, n.id);
1850
+ const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
1851
+ const portsT = batched ? `${portsStructName(c.name, n.id)}Batch` : portsStructName(c.name, n.id);
1852
+ const retT = batched ? batchT : elemT;
1853
+ if (batched) {
1854
+ methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
1855
+ let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
1856
+ if is_err {
1857
+ return Some(${batchT} { is_error: true, err, ..Default::default() });
1858
+ }
1859
+ let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
1860
+ let rows = arr.iter().map(${elemDecode}).collect();
1861
+ Some(${batchT} { rows, ..Default::default() })
1862
+ }`);
1863
+ }
1864
+ else {
1865
+ methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${elemT}> {
1866
+ let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
1867
+ if is_err {
1868
+ return Some(${elemT} { is_error: true, err, ..Default::default() });
1869
+ }
1870
+ let v = ok.unwrap_or(Value::Null);
1871
+ Some(${elemDecode}(&v))
1872
+ }`);
1873
+ }
1874
+ void retT;
1875
+ continue;
1876
+ }
1877
+ const node = n;
1878
+ const rowT = rawRowStructName(c.name, node.id);
1879
+ const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
1880
+ methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, _ports: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
1881
+ let (is_err, err, ok) = self.next(${rustStrLit(node.component)})?;
1882
+ if is_err {
1883
+ return Some(${rowT} { is_error: true, err, ..Default::default() });
1884
+ }
1885
+ let v = ok.unwrap_or(Value::Null);
1886
+ Some(${decode}(&v))
1887
+ }`);
1888
+ }
1889
+ traitImpls.push(`impl ${handlerTraitName(c.name)} for ScriptedNativeRaw {\n${methods.join("\n")}\n}`);
1890
+ }
1891
+ const arms = native
1892
+ .map((c) => {
1893
+ const outRef = c.outputType !== undefined ? deriveTypeRef(c.outputType, plan) : undefined;
1894
+ const ser = outRef ? serializeTyped("tv", outRef, plan) : "tv";
1895
+ // bc#90 / runtime-free: the covered runner returns the module-LOCAL `BehaviorError` (via super::*);
1896
+ // bridge it to the bc-runtime `behavior_contracts::BehaviorError` at the observe boundary (same
1897
+ // code/message) so the conformance harness (main.rs) reads the stable code without the covered
1898
+ // module ever touching bc runtime. decode_* is test glue and returns the local error too.
1899
+ // bc#97: an async runner returns a Future — drive it to completion with the std-only block-on
1900
+ // (bc_block_on) so observe_native_raw keeps its SYNC external signature (the harness is unchanged).
1901
+ // The scripted async node_* return ready values, so the future needs no real executor / no runtime.
1902
+ const runnerCall = `super::run_native_raw_struct_${sanitize(c.name)}(handlers, in_)`;
1903
+ const driven = asyncPlans.get(c.name).runnerIsAsync ? `bc_block_on(${runnerCall})` : runnerCall;
1904
+ return ` ${rustStrLit(c.name)} => {\n let in_ = decode_${inStructName(c.name)}(input).map_err(to_rt_err)?;\n let tv = ${driven}.map_err(to_rt_err)?;\n Ok(${ser})\n }`;
1905
+ })
1906
+ .join("\n");
1907
+ // the scripted handler is passed as `&ScriptedNativeRaw`; the observe entry takes `&ScriptedNativeRaw`
1908
+ // directly (the harness constructs it). Sync is inherent (the queues live behind a Mutex, seen behind
1909
+ // an atomic) so a concurrency stage runs it in parallel.
1910
+ const adapter = `// ScriptedNativeRaw — the CONCRETE test handler: it satisfies every HandlerNR<comp> trait by draining a
1911
+ // per-COMPONENT scripted-outcome queue and decoding the scripted ok Value into the concrete row struct.
1912
+ // The decode (decode_row_*) is generated (it knows the concrete row type), so the runner stays fully
1913
+ // concrete. Sync (queues behind a Mutex, the probe counter an atomic) so a bc#87 stage parallelizes it.
1914
+ pub struct ScriptedNativeRaw {
1915
+ queues: std::sync::Mutex<std::collections::HashMap<String, (Vec<serde_json::Value>, bool)>>,
1916
+ #[allow(dead_code)]
1917
+ seen: std::sync::atomic::AtomicUsize,
1918
+ }
1919
+
1920
+ impl ScriptedNativeRaw {
1921
+ // build from the JSON handlers spec: { "<component>": {ok|error} | [ {ok|error}, … ] }.
1922
+ pub fn new(spec: &serde_json::Map<String, serde_json::Value>) -> Self {
1923
+ let mut queues = std::collections::HashMap::new();
1924
+ for (name, raw) in spec {
1925
+ let (items, single) = match raw {
1926
+ serde_json::Value::Array(a) => (a.clone(), false),
1927
+ other => (vec![other.clone()], true),
1928
+ };
1929
+ queues.insert(name.clone(), (items, single));
1930
+ }
1931
+ ScriptedNativeRaw {
1932
+ queues: std::sync::Mutex::new(queues),
1933
+ seen: std::sync::atomic::AtomicUsize::new(0),
1934
+ }
1935
+ }
1936
+
1937
+ // drain one scripted outcome for a component (last entry repeats — mirrors the reference scripted
1938
+ // handlers); None when the component has no scripted queue (fail-closed). Returns (is_error, err,
1939
+ // ok_value). The bc#87 parallel-stage runner calls node_* from multiple threads, so the drain is
1940
+ // Mutex-guarded.
1941
+ fn next(&self, component: &str) -> Option<(bool, String, Option<Value>)> {
1942
+ self.seen.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1943
+ let mut queues = self.queues.lock().unwrap();
1944
+ let (queue, single) = queues.get_mut(component)?;
1945
+ let raw = if *single || queue.len() <= 1 { queue[0].clone() } else { queue.remove(0) };
1946
+ if let Some(okv) = raw.get("ok") {
1947
+ let v = behavior_contracts::decode_value(okv).expect("decode ok");
1948
+ Some((false, String::new(), Some(v)))
1949
+ } else {
1950
+ let e = raw.get("error").and_then(|e| e.as_str()).unwrap_or("").to_string();
1951
+ Some((true, e, None))
1952
+ }
1953
+ }
1954
+ }`;
1955
+ const observeBound = needSync ? "" : "";
1956
+ void observeBound;
1957
+ return `// GENERATED test-only observe companion (bc#77/#94) — NOT part of the covered module. Serializes the
1958
+ // struct-native runner's result to a canonical Value for the equivalence pin, and carries the CONCRETE
1959
+ // scripted test handler (ScriptedNativeRaw) that satisfies every HandlerNR<comp> trait by decoding a
1960
+ // scripted Value into the concrete per-node row. The consumer never uses this (it keeps the model native
1961
+ // + implements HandlerNR<comp> over its own wire payload); it exists only so the test harness can
1962
+ // byte-compare the STRUCT-only covered read against run_behavior. A submodule of the covered module
1963
+ // (reads its pub run_native_raw_struct_* + the CONCRETE structs/traits — incl. the LOCAL BehaviorError —
1964
+ // via super::*). bc#90 / runtime-free: the covered module's BehaviorError is LOCAL, so this test-glue
1965
+ // companion refers to it via super::* (as \`BehaviorError\`) and refers to the bc-runtime failure type
1966
+ // FULLY-QUALIFIED (\`behavior_contracts::BehaviorError\`) only where it bridges at the observe boundary.
1967
+ #![allow(dead_code, unused_imports)]
1968
+ use behavior_contracts::Value;
1969
+ use super::*;
1970
+
1971
+ // bridge the covered module's LOCAL BehaviorError (super::BehaviorError) to the bc-runtime failure type
1972
+ // the conformance harness reads (same code + message) — the ONLY place the two meet (test glue).
1973
+ fn to_rt_err(e: BehaviorError) -> behavior_contracts::BehaviorError {
1974
+ behavior_contracts::BehaviorError::new(e.code, e.message)
1975
+ }
1976
+ ${anyAsyncRunner
1977
+ ? `// bc#97 (TEST glue) — a minimal std-only block-on that drives an async runner Future to completion so
1978
+ // observe_native_raw stays sync-callable (the harness never changes). The covered async fn's futures are
1979
+ // ready (the scripted handler does no real I/O), so a no-op waker + spin poll resolves in one step. This
1980
+ // lives ONLY in the observe companion — the covered module itself imports zero runtime.
1981
+ fn bc_block_on<F: std::future::Future>(mut fut: F) -> F::Output {
1982
+ use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
1983
+ fn noop(_: *const ()) {}
1984
+ fn clone(_: *const ()) -> RawWaker {
1985
+ RawWaker::new(std::ptr::null(), &VTABLE)
1986
+ }
1987
+ static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);
1988
+ let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
1989
+ let mut cx = Context::from_waker(&waker);
1990
+ // SAFETY: fut is owned on the stack and never moved after pinning (it is dropped at scope end).
1991
+ let mut fut = unsafe { std::pin::Pin::new_unchecked(&mut fut) };
1992
+ loop {
1993
+ match fut.as_mut().poll(&mut cx) {
1994
+ Poll::Ready(v) => return v,
1995
+ Poll::Pending => std::thread::yield_now(),
1996
+ }
1997
+ }
1998
+ }`
1999
+ : ""}
2000
+
2001
+ ${serializers}
2002
+
2003
+ // Value -> typed struct marshallers for the concrete-row decode (TEST glue only).
2004
+ ${marshallers}
2005
+
2006
+ ${emitMarshalHelpers()}
2007
+
2008
+ // per-node concrete-row decoders (scripted Value -> RawRowNR_/RawElemNR_ struct).
2009
+ ${decodeFns.join("\n\n")}
2010
+
2011
+ // input decoders (generic &[(String,Value)] -> concrete InNR_<comp>; TEST glue, one decode at the boundary).
2012
+ ${inDecoders}
2013
+
2014
+ ${adapter}
2015
+
2016
+ // scripted handler trait impls (one per covered component — the concrete node_* seam, test glue).
2017
+ ${traitImpls.join("\n\n")}
2018
+
2019
+ // observe_native_raw drives the CONCRETE struct-native runner and serializes to a canonical Value (test
2020
+ // only). Takes the CONCRETE &ScriptedNativeRaw handler so each enclosed run_native_raw_struct_* call is
2021
+ // instantiated at the concrete handler type (direct per-node method calls), not at a trait object.
2022
+ pub fn observe_native_raw(
2023
+ name: &str,
2024
+ handlers: &ScriptedNativeRaw,
2025
+ input: &[(String, Value)],
2026
+ ) -> Result<Value, behavior_contracts::BehaviorError> {
2027
+ match name {
2028
+ ${arms}
2029
+ other => Err(behavior_contracts::BehaviorError::new("UNKNOWN_ENTRY", format!("component '{other}' is not a covered read (fail-closed)"))),
2030
+ }
2031
+ }
2032
+ `;
2033
+ }
2034
+ /** emitInDecoder — a TEST-glue decoder `decode_InNR_<comp>(input) -> Result<InNR_<comp>, BehaviorError>`.
2035
+ * Reads each declared input port from the generic &[(String,Value)] into the concrete field (scalar →
2036
+ * typed read; Value-typed → the raw Value clone). A missing key leaves Default (every covered input is
2037
+ * required + supplied by the corpus). Lives ONLY in the observe companion. */
2038
+ function emitInDecoder(comp, plan) {
2039
+ const name = inStructName(comp.name);
2040
+ const ports = Object.keys(comp.inputPorts ?? {});
2041
+ const lines = [];
2042
+ lines.push(`fn decode_${name}(input: &[(String, Value)]) -> Result<${name}, BehaviorError> {`);
2043
+ lines.push(` let mut in_ = ${name}::default();`);
2044
+ if (ports.length === 0 || comp.inputPorts === undefined) {
2045
+ lines.push(` let _ = input;`);
2046
+ lines.push(` Ok(in_)`);
2047
+ lines.push(`}`);
2048
+ return lines.join("\n");
2049
+ }
2050
+ // an if/else-if chain (clippy-clean regardless of port count — a single-arm `match` trips single_match).
2051
+ lines.push(` for (k, v) in input {`);
2052
+ ports.forEach((p, i) => {
2053
+ const field = rustFieldName(p);
2054
+ const schema = comp.inputPorts[p];
2055
+ const scalar = inputScalarKind(schema);
2056
+ const et = schema.elemType;
2057
+ const head = i === 0 ? " if" : " } else if";
2058
+ lines.push(`${head} k == ${rustStrLit(p)} {`);
2059
+ if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
2060
+ // #108: decode the boxed Value::Arr into the native Vec<ElemT> (test glue — materialize each elem).
2061
+ const elemRef = deriveTypeRef(et, plan);
2062
+ const elemTy = renderTypeRef(elemRef);
2063
+ const mat = materializeExpr("ev", elemRef, plan);
2064
+ lines.push(` match v {`);
2065
+ lines.push(` Value::Arr(a) => {`);
2066
+ lines.push(` let mut dec: Vec<${elemTy}> = Vec::with_capacity(a.len());`);
2067
+ lines.push(` for ev in a.iter() { dec.push(${mat}); }`);
2068
+ lines.push(` in_.${field} = dec;`);
2069
+ lines.push(` }`);
2070
+ lines.push(` other => return Err(marshal_type_error(${rustStrLit(`decode input ${p}: expected array, got `)}, other.type_name())),`);
2071
+ lines.push(` }`);
2072
+ }
2073
+ else if (scalar === undefined) {
2074
+ lines.push(` in_.${field} = v.clone();`);
2075
+ }
2076
+ else {
2077
+ const pat = scalar === "String" ? "Value::Str(sv)" : scalar === "i64" ? "Value::Int(sv)" : scalar === "f64" ? "Value::Float(sv)" : "Value::Bool(sv)";
2078
+ const label = scalar === "String" ? "string" : scalar === "i64" ? "int" : scalar === "f64" ? "float" : "bool";
2079
+ const bind = scalar === "String" ? "sv.clone()" : "*sv";
2080
+ // bc#90 / runtime-free: build the module-LOCAL BehaviorError (TYPE_MISMATCH) via the local
2081
+ // marshal_type_error helper — decode_* returns the local error, bridged at the observe boundary.
2082
+ lines.push(` match v { ${pat} => in_.${field} = ${bind}, other => return Err(marshal_type_error(${rustStrLit(`decode input ${p}: expected ${label}, got `)}, other.type_name())) }`);
2083
+ }
2084
+ });
2085
+ lines.push(` }`);
2086
+ lines.push(` }`);
2087
+ lines.push(` Ok(in_)`);
2088
+ lines.push(`}`);
2089
+ return lines.join("\n");
2090
+ }
2091
+ /** emitDecodeRow — emit (once, deduped) a decoder `decode_row_<comp>_<node><suffix>(v: &Value) -> <rowT>`
2092
+ * that materializes the scripted ok Value into the concrete row struct. Named: reuse the Value->struct
2093
+ * marshaller + move fields; scalar/arr/opt: materialize into .val. TEST glue only. */
2094
+ function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix) {
2095
+ const fn = `decode_row_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}${suffix.toLowerCase()}`;
2096
+ if (emitted.has(fn))
2097
+ return fn;
2098
+ emitted.add(fn);
2099
+ const lines = [];
2100
+ lines.push(`fn ${fn}(v: &Value) -> ${rowT} {`);
2101
+ if (ref.kind === "named") {
2102
+ const decl = findDecl(plan, ref.name);
2103
+ lines.push(` let t = marshal_${ref.name}(v).expect("scripted row decode");`);
2104
+ const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: t.${rustFieldName(f.name)}`).join(", ");
2105
+ lines.push(` ${rowT} { ${inits}, ..Default::default() }`);
2106
+ }
2107
+ else {
2108
+ const mat = materializeExpr("v", ref, plan);
2109
+ lines.push(` let val = (|| -> Result<${renderTypeRef(ref)}, BehaviorError> { Ok(${mat}) })().expect("scripted row decode");`);
2110
+ lines.push(` ${rowT} { val, ..Default::default() }`);
2111
+ }
2112
+ lines.push(`}`);
2113
+ out.push(lines.join("\n"));
2114
+ return fn;
2115
+ }
2116
+ export const rustTypedNativeEmitter = {
2117
+ language: "rust-typed-native",
2118
+ fileExtension: ".rs",
2119
+ defaultRuntimeImport: "behavior_contracts",
2120
+ filenameHint: "behaviors_typed_native_generated.rs",
2121
+ emit,
2122
+ };
2123
+ //# sourceMappingURL=emit-typed-native-rust.js.map