behavior-contracts 0.4.0 → 0.5.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 (51) hide show
  1. package/README.md +78 -29
  2. package/dist/generator/core.d.ts +14 -0
  3. package/dist/generator/core.d.ts.map +1 -1
  4. package/dist/generator/core.js +1 -0
  5. package/dist/generator/core.js.map +1 -1
  6. package/dist/generator/emit-raw-abi-go.d.ts.map +1 -1
  7. package/dist/generator/emit-raw-abi-go.js +2 -1
  8. package/dist/generator/emit-raw-abi-go.js.map +1 -1
  9. package/dist/generator/emit-straightline-go.d.ts +2 -0
  10. package/dist/generator/emit-straightline-go.d.ts.map +1 -1
  11. package/dist/generator/emit-straightline-go.js +37 -19
  12. package/dist/generator/emit-straightline-go.js.map +1 -1
  13. package/dist/generator/emit-straightline-native-go.d.ts +54 -0
  14. package/dist/generator/emit-straightline-native-go.d.ts.map +1 -0
  15. package/dist/generator/emit-straightline-native-go.js +835 -0
  16. package/dist/generator/emit-straightline-native-go.js.map +1 -0
  17. package/dist/generator/emit-straightline-native-rust.d.ts +27 -0
  18. package/dist/generator/emit-straightline-native-rust.d.ts.map +1 -0
  19. package/dist/generator/emit-straightline-native-rust.js +624 -0
  20. package/dist/generator/emit-straightline-native-rust.js.map +1 -0
  21. package/dist/generator/emit-straightline-rust.d.ts.map +1 -1
  22. package/dist/generator/emit-straightline-rust.js +5 -12
  23. package/dist/generator/emit-straightline-rust.js.map +1 -1
  24. package/dist/generator/emit-straightline-typed-go.d.ts +17 -0
  25. package/dist/generator/emit-straightline-typed-go.d.ts.map +1 -1
  26. package/dist/generator/emit-straightline-typed-go.js +4 -1
  27. package/dist/generator/emit-straightline-typed-go.js.map +1 -1
  28. package/dist/generator/emit-straightline-typed-rust.d.ts +16 -0
  29. package/dist/generator/emit-straightline-typed-rust.d.ts.map +1 -1
  30. package/dist/generator/emit-straightline-typed-rust.js +2 -0
  31. package/dist/generator/emit-straightline-typed-rust.js.map +1 -1
  32. package/dist/generator/emit-straightline-typescript.d.ts.map +1 -1
  33. package/dist/generator/emit-straightline-typescript.js +4 -10
  34. package/dist/generator/emit-straightline-typescript.js.map +1 -1
  35. package/dist/generator/emit-typed-native-go.d.ts +63 -0
  36. package/dist/generator/emit-typed-native-go.d.ts.map +1 -0
  37. package/dist/generator/emit-typed-native-go.js +1840 -0
  38. package/dist/generator/emit-typed-native-go.js.map +1 -0
  39. package/dist/generator/emit-typed-native-rust.d.ts +52 -0
  40. package/dist/generator/emit-typed-native-rust.d.ts.map +1 -0
  41. package/dist/generator/emit-typed-native-rust.js +1673 -0
  42. package/dist/generator/emit-typed-native-rust.js.map +1 -0
  43. package/dist/generator/index.d.ts +2 -0
  44. package/dist/generator/index.d.ts.map +1 -1
  45. package/dist/generator/index.js +16 -0
  46. package/dist/generator/index.js.map +1 -1
  47. package/dist/generator/straightline.d.ts +46 -0
  48. package/dist/generator/straightline.d.ts.map +1 -1
  49. package/dist/generator/straightline.js +65 -0
  50. package/dist/generator/straightline.js.map +1 -1
  51. package/package.json +1 -1
@@ -0,0 +1,1673 @@
1
+ import { GeneratorFailure } from "./core.js";
2
+ import { rustStraightlineInternals } from "./emit-straightline-rust.js";
3
+ import { rustTypedInternals } from "./emit-straightline-typed-rust.js";
4
+ import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
5
+ import { concurrencyPlan, execPlan, analyzeSequentialOps, classifyExpr } from "./straightline.js";
6
+ const { sanitize, rustStrLit } = rustStraightlineInternals;
7
+ const { rustFieldName, renderTypeRef, typedCell, typedFieldAccess, serializeTyped, emitTypeDecls, emitDefaults, emitSerializers, emitTypedValue, emitMarshallers, emitMarshalHelpers, materializeExpr, } = rustTypedInternals;
8
+ /** rustErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
9
+ * plane (runtime-free). The covered module declares a local `BehaviorError` (see emitBehaviorErrorType);
10
+ * the runner returns `Result<_, BehaviorError>` over THAT local type. This REPLACES the old
11
+ * `behavior_contracts::BehaviorError::new(...)` (the last error-plane runtime coupling) — the SAME codes
12
+ * are preserved verbatim so the observed failure is byte-equal to run_behavior. */
13
+ function rustErr(code, msgExpr) {
14
+ return `BehaviorError::new(${JSON.stringify(code)}, ${msgExpr})`;
15
+ }
16
+ /** emitBehaviorErrorType — the LOCAL concrete error type baked into the covered module so a runner can
17
+ * fail closed with ZERO bc-runtime import. Codes match run_behavior verbatim (byte-equal). `code()` /
18
+ * `failure_code()` expose the stable code without a bc-runtime type. */
19
+ function emitBehaviorErrorType() {
20
+ return `// BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
21
+ // covered runner returns \`Result<T, BehaviorError>\` over THIS local type instead of a bc-runtime
22
+ // failure, so the fully-covered module imports ZERO bc runtime. Codes match run_behavior verbatim
23
+ // (byte-equal). The observe companion (test glue, a super:: submodule) bridges this local error to the
24
+ // bc-runtime BehaviorError at the observe boundary — the covered module itself stays runtime-free.
25
+ #[derive(Debug, Clone)]
26
+ pub struct BehaviorError {
27
+ pub code: String,
28
+ pub message: String,
29
+ }
30
+
31
+ impl BehaviorError {
32
+ pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
33
+ BehaviorError { code: code.into(), message: message.into() }
34
+ }
35
+ /// The stable failure code (byte-equal to run_behavior) WITHOUT a bc-runtime type.
36
+ pub fn code(&self) -> &str {
37
+ &self.code
38
+ }
39
+ }
40
+
41
+ impl std::fmt::Display for BehaviorError {
42
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43
+ write!(f, "{}: {}", self.code, self.message)
44
+ }
45
+ }
46
+ impl std::error::Error for BehaviorError {}`;
47
+ }
48
+ function portFieldName(wire) {
49
+ const safe = wire.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase();
50
+ return `f_${safe}`;
51
+ }
52
+ function pascal(s) {
53
+ return s.replace(/[^A-Za-z0-9]+/g, " ").split(" ").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") || "X";
54
+ }
55
+ function portsStructName(compName, nodeId) {
56
+ return `PortsNR${pascal(compName)}${pascal(nodeId)}`;
57
+ }
58
+ /** RawRowNR<Comp><Node> — the concrete per-node handler-result row struct name. */
59
+ function rawRowStructName(compName, nodeId) {
60
+ return `RawRowNR${pascal(compName)}${pascal(nodeId)}`;
61
+ }
62
+ /** RawElemNR<Comp><Node> — the concrete per-ELEMENT row struct name for a covered map node. */
63
+ function rawElemStructName(compName, nodeId) {
64
+ return `RawElemNR${pascal(compName)}${pascal(nodeId)}`;
65
+ }
66
+ /** HandlerNR<Comp> — the per-component concrete handler trait name (one node_* method per node). */
67
+ function handlerTraitName(compName) {
68
+ return `HandlerNR${pascal(compName)}`;
69
+ }
70
+ /** node_<id> — the concrete per-node handler method name. */
71
+ function nodeMethodName(nodeId) {
72
+ return `node_${nodeId.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase()}`;
73
+ }
74
+ /** InNR<Comp> — the concrete per-component input struct name (fields = inputPorts). */
75
+ function inStructName(compName) {
76
+ return `InNR${pascal(compName)}`;
77
+ }
78
+ function staticArrElems(node) {
79
+ if (node === null || typeof node !== "object" || Array.isArray(node))
80
+ return null;
81
+ const keys = Object.keys(node);
82
+ if (keys.length !== 1 || keys[0] !== "arr")
83
+ return null;
84
+ const arg = node.arr;
85
+ return Array.isArray(arg) ? arg : null;
86
+ }
87
+ function inferPortType(node, inputPorts) {
88
+ if (staticArrElems(node) !== null)
89
+ return "Value";
90
+ const e = classifyExpr(node);
91
+ switch (e.kind) {
92
+ case "str":
93
+ case "concat":
94
+ return "String";
95
+ case "bool":
96
+ return "bool";
97
+ case "ref": {
98
+ if (e.path.length === 1) {
99
+ const s = inputPorts[e.path[0]];
100
+ if (s) {
101
+ if (s.type === "string")
102
+ return "String";
103
+ if (s.type === "int")
104
+ return "i64";
105
+ if (s.type === "float")
106
+ return "f64";
107
+ if (s.type === "bool")
108
+ return "bool";
109
+ }
110
+ }
111
+ return "Value";
112
+ }
113
+ default:
114
+ return "Value";
115
+ }
116
+ }
117
+ /** A static string-array port (every element a static string literal) — the graphddb `projection`
118
+ * shape. Returns the list of literal strings, or null when the node is not that shape. */
119
+ function staticStringArrElems(node) {
120
+ const arr = staticArrElems(node);
121
+ if (arr === null)
122
+ return null;
123
+ const out = [];
124
+ for (const el of arr) {
125
+ const e = classifyExpr(el);
126
+ if (e.kind !== "str")
127
+ return null;
128
+ out.push(e.value);
129
+ }
130
+ return out;
131
+ }
132
+ /**
133
+ * portFieldRustType — the NATIVE Rust type for a covered port field (bc#90 / runtime-free). The covered
134
+ * ports struct carries NO `Value` field: every port lowers to a concrete Rust type.
135
+ * - a static string-array (projection) → `Vec<&'static str>` (change #2);
136
+ * - a bare int/float number literal (e.g. Query `limit: 20`) → `i64` / `f64`;
137
+ * - a string/bool/scalar-input port → the scalar Rust type (inferPortType).
138
+ * A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
139
+ * Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no `Value`
140
+ * fallback on the native module). `where` names the emission site for the error.
141
+ */
142
+ function portFieldRustType(node, inputPorts, where = "port") {
143
+ if (staticStringArrElems(node) !== null)
144
+ return "Vec<&'static str>";
145
+ const num = numLiteralRustExpr(node);
146
+ if (num !== null)
147
+ return typeof node === "number" && Number.isInteger(node) ? "i64" : "f64";
148
+ const ft = inferPortType(node, inputPorts);
149
+ if (ft === "Value") {
150
+ 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.`);
151
+ }
152
+ return ft;
153
+ }
154
+ /**
155
+ * emitPortsStruct — the CONCRETE native ports struct for a componentRef node. Typed fields per the
156
+ * static port type, constructed directly. NO `impl PortReaderT` / by-name `port()` — the covered runner
157
+ * passes `&PortsNR…` to the concrete `node_*` method by direct generic instantiation, and the handler
158
+ * reads `ports.f_<field>` DIRECTLY. The struct derives Clone (a bc#87 parallel stage clones it into the
159
+ * per-worker dispatch). Every field is a NATIVE Rust type (bc#90 / runtime-free) — NO boxed `Value`.
160
+ */
161
+ function emitPortsStruct(comp, node) {
162
+ const name = portsStructName(comp.name, node.id);
163
+ const portNames = Object.keys(node.ports);
164
+ if (portNames.length === 0) {
165
+ return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
166
+ }
167
+ const types = portNames.map((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`));
168
+ const fields = portNames
169
+ .map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
170
+ .join("\n");
171
+ return `// ${name} — CONCRETE native ports for node '${node.id}' (${node.component}). Typed fields per the
172
+ // static port type; constructed directly (no Vec, no heap key strings, no per-port Value boxing). The
173
+ // handler reads the typed fields directly off this concrete struct — no by-name accessor.
174
+ #[derive(Clone)]
175
+ pub struct ${name} {
176
+ ${fields}
177
+ }`;
178
+ }
179
+ /**
180
+ * emitMapPortsStructs — element ports struct + batch struct for a covered map node (CONCRETE). The batch
181
+ * struct carries the Vec of per-element CONCRETE ports structs (the handler reads them off the concrete
182
+ * struct — no by-name accessor, no key-value object).
183
+ */
184
+ function emitMapPortsStructs(comp, node) {
185
+ const m = node.map;
186
+ const name = portsStructName(comp.name, node.id);
187
+ const batch = `${name}Batch`;
188
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports });
189
+ return `${elemStruct}
190
+
191
+ // ${batch} — CONCRETE batched ports for map '${node.id}': the Vec of per-element CONCRETE ports structs
192
+ // (a Vec<${name}>, NOT a generic key-value container). The consumer's batched handler reads the typed
193
+ // element ports off the concrete struct directly.
194
+ #[derive(Clone)]
195
+ pub struct ${batch} {
196
+ pub items: Vec<${name}>,
197
+ }`;
198
+ }
199
+ // ── CONCRETE per-node row structs + row→outType copiers (mirror the Go concrete contract) ──────────
200
+ //
201
+ // Each covered node has a CONCRETE row struct `RawRowNR<Comp><Node>` whose fields are the node's projected
202
+ // outType fields (typed, flattened) plus a per-node error signal (is_error/err). A named-struct outType
203
+ // flattens to one Rust field per outType field; a scalar/arr/opt node carries a single `val: <typed>`
204
+ // payload. The per-component `HandlerNR<Comp>` trait has one method per node returning that concrete
205
+ // struct; the runner reads `row.<field>` DIRECTLY (no `RawValue`, no `match`, no `Value::`) and copies
206
+ // each field into the node's outType struct local.
207
+ /** emitRawRowStructs — emit the concrete `RawRowNR<Comp><Node>` struct per covered node (+ per-element
208
+ * `RawElemNR<Comp><Node>` for a covered map). Every row carries the per-node error signal (is_error/err). */
209
+ function emitRawRowStructs(comp, typedNodes, plan) {
210
+ const parts = [];
211
+ for (const n of comp.body) {
212
+ const ref = typedNodes.get(n.id);
213
+ if ("map" in n) {
214
+ const arrRef = ref;
215
+ const elemRowRef = mapElemRowRef(n, arrRef, plan);
216
+ parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
217
+ parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
218
+ `#[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}`);
219
+ continue;
220
+ }
221
+ parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
222
+ }
223
+ return parts.join("\n\n");
224
+ }
225
+ /** Emit one concrete row struct: flatten a named outType to typed fields, else a single `val` payload;
226
+ * always carry is_error/err. Derives Default so a handler can build an error row cheaply (`..Default`). */
227
+ function emitOneRowStruct(name, ref, plan) {
228
+ const lines = [];
229
+ lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType; + error signal).`);
230
+ lines.push(`#[derive(Default)]`);
231
+ lines.push(`pub struct ${name} {`);
232
+ lines.push(` pub is_error: bool,`);
233
+ lines.push(` pub err: String,`);
234
+ if (ref.kind === "named") {
235
+ const decl = findDecl(plan, ref.name);
236
+ for (const f of decl.fields) {
237
+ lines.push(` pub ${rustFieldName(f.name)}: ${renderTypeRef(f.type)}, // ${JSON.stringify(f.name)}`);
238
+ }
239
+ }
240
+ else {
241
+ lines.push(` pub val: ${renderTypeRef(ref)},`);
242
+ }
243
+ lines.push(`}`);
244
+ return lines.join("\n");
245
+ }
246
+ /** emitRowCopy — the statement copying a concrete row's typed fields into the node's outType local
247
+ * `target` (a value of Rust type renderTypeRef(ref)). Named: struct literal from the row's moved fields;
248
+ * scalar/arr/opt: `.val`. `rowExpr` is the concrete row VALUE (moved). No dynamic read — pure move/copy. */
249
+ function emitRowMove(target, rowExpr, ref, plan) {
250
+ if (ref.kind === "named") {
251
+ const decl = findDecl(plan, ref.name);
252
+ const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: ${rowExpr}.${rustFieldName(f.name)}`).join(", ");
253
+ return `${target} = ${ref.name} { ${inits} };`;
254
+ }
255
+ return `${target} = ${rowExpr}.val;`;
256
+ }
257
+ /**
258
+ * boundRustType (change #3 — typed `bound`) — the CONCRETE Rust type of a covered node's `bound` handler
259
+ * param (bc#90 / runtime-free). `bound` is the parent-produced key (the bindField value); it replaces the
260
+ * old boxed `Value`. We represent PRODUCED-vs-UNPRODUCED with an OWNED `Option<K>`: `Some(v)` = the parent
261
+ * value was produced/present (an empty string is a VALID produced value — `Some("")`); `None` =
262
+ * UNPRODUCED/skip (exactly the old `!= Null` guard ≡ run_behavior). `K` is the bindField's concrete Rust
263
+ * type (a string key here). A node WITHOUT a bindField never receives a bound value — its param is an
264
+ * `Option<String>` placeholder always passed `None` (a typed None, so the ABI stays uniform and carries
265
+ * NO Value).
266
+ */
267
+ function boundRustType(node, typedNodes, plan) {
268
+ if (node.bindField === undefined || node.parent === undefined)
269
+ return "Option<String>";
270
+ const parentRef = typedNodes.get(node.parent);
271
+ if (parentRef === undefined)
272
+ return "Option<String>";
273
+ let bfRef;
274
+ try {
275
+ bfRef = typedFieldAccess(`${typedCell(node.parent)}.borrow()`, parentRef, [node.bindField], plan).ref;
276
+ }
277
+ catch {
278
+ return "Option<String>";
279
+ }
280
+ // opt bindField: the field is already `Option<T>` — the bound is that same Option. A required scalar
281
+ // bindField: the bound is `Option<T>` (always Some — the "produced" pointer is never None).
282
+ if (bfRef.kind === "opt")
283
+ return `Option<${renderTypeRef(bfRef.inner)}>`;
284
+ return `Option<${renderTypeRef(bfRef)}>`;
285
+ }
286
+ /**
287
+ * emitHandlerTrait — the per-component `HandlerNR<Comp>` trait: one concrete-typed `node_*` method per
288
+ * covered node. A componentRef node method takes the node's native ports struct + the typed produced-aware
289
+ * `bound` (change #3 — NO boxed Value) and returns Option<RawRowNR…> (None = unknown component,
290
+ * fail-closed). A covered map node's method takes the batch ports struct (batched) or the element ports
291
+ * struct (per-element). This is the fully-concrete, runtime-free replacement for the NativeRawComponentExec
292
+ * seam.
293
+ */
294
+ function emitHandlerTrait(comp, typedNodes, plan, isAsync = false) {
295
+ // bc#97: async I/O model (uniform) — every terminal handler method is an `async fn` so a consumer's
296
+ // async node_* returns a future that the (async) runner awaits at the call site. Default = plain `fn`
297
+ // (byte-identical to the pre-#97 sync output). `async fn` needs NO external runtime to compile — the
298
+ // future is `async fn` sugar over std, so the covered module stays runtime-free either way.
299
+ const fnKw = isAsync ? "async fn" : "fn";
300
+ const lines = [];
301
+ lines.push(`// ${handlerTraitName(comp.name)} — the CONCRETE per-component handler seam: one typed method`);
302
+ lines.push(`// per covered node (native ports struct IN, concrete row struct OUT). No generic boxed-ports`);
303
+ lines.push(`// / boxed-value / dynamic accessor crosses the covered boundary — the consumer implements each`);
304
+ lines.push(`// node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
305
+ lines.push(`pub trait ${handlerTraitName(comp.name)} {`);
306
+ for (const n of comp.body) {
307
+ const method = nodeMethodName(n.id);
308
+ const bt = boundRustType(n, typedNodes, plan);
309
+ if ("map" in n) {
310
+ const m = n.map;
311
+ const batched = m.batched === true;
312
+ const portsT = batched ? `${portsStructName(comp.name, n.id)}Batch` : portsStructName(comp.name, n.id);
313
+ const retT = batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
314
+ lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Option<${retT}>;`);
315
+ continue;
316
+ }
317
+ lines.push(` ${fnKw} ${method}(&self, ports: &${portsStructName(comp.name, n.id)}, bound: ${bt}) -> Option<${rawRowStructName(comp.name, n.id)}>;`);
318
+ }
319
+ lines.push(`}`);
320
+ return lines.join("\n");
321
+ }
322
+ /** mapElemMaterializerName — the augmented-element copier for a covered map...into node. */
323
+ function mapElemMaterializerName(nodeId) {
324
+ return `materialize_map_elem_${typedCell(nodeId)}`;
325
+ }
326
+ /** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
327
+ * type (into) or the element outType directly (no-into). This is the type of RawElemNR<Comp><Node>. */
328
+ function mapElemRowRef(node, arrRef, plan) {
329
+ const m = node.map;
330
+ if (m.into === undefined)
331
+ return arrRef.elem;
332
+ if (arrRef.elem.kind !== "named") {
333
+ 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.`);
334
+ }
335
+ const decl = findDecl(plan, arrRef.elem.name);
336
+ const intoField = decl.fields.find((f) => f.name === m.into);
337
+ if (!intoField) {
338
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined map (#86 pt2): element struct '${arrRef.elem.name}' has no '${m.into}' (into) field.`);
339
+ }
340
+ return intoField.type;
341
+ }
342
+ /**
343
+ * emitMapElemMaterializers — for each covered map...into node, emit the AUGMENTED-element copier: copy
344
+ * the over element's typed fields + move the `into` field from the concrete per-element handler row
345
+ * (RawElemNR). Pure struct field assignment — NO dynamic value read (the handler already produced the
346
+ * typed into row).
347
+ */
348
+ function emitMapElemMaterializers(comp, typedNodes, plan) {
349
+ const parts = [];
350
+ for (const n of comp.body) {
351
+ if (!("map" in n))
352
+ continue;
353
+ const m = n.map;
354
+ // #93 shape 3: a NO-into map materializes each element directly from the per-element row.
355
+ if (m.into === undefined)
356
+ continue;
357
+ const arrRef = typedNodes.get(n.id);
358
+ if (arrRef.kind !== "arr" || arrRef.elem.kind !== "named") {
359
+ 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.`);
360
+ }
361
+ const elemRef = arrRef.elem;
362
+ const decl = findDecl(plan, elemRef.name);
363
+ const intoKey = m.into;
364
+ const intoField = decl.fields.find((f) => f.name === intoKey);
365
+ const overFields = decl.fields.filter((f) => f.name !== intoKey);
366
+ const overElemTy = mapOverElemType(comp, n, typedNodes, plan);
367
+ const fn = mapElemMaterializerName(n.id);
368
+ const elemRowGo = rawElemStructName(comp.name, n.id);
369
+ const intoRowRef = mapElemRowRef(n, arrRef, plan);
370
+ const inits = [];
371
+ for (const f of overFields)
372
+ inits.push(` ${rustFieldName(f.name)}: over.${rustFieldName(f.name)}.clone(),`);
373
+ // move the into field off the concrete element row (named → struct literal; scalar/arr/opt → .val).
374
+ const intoValue = intoRowRef.kind === "named"
375
+ ? `${intoRowRef.name} { ${findDecl(plan, intoRowRef.name).fields.map((f) => `${rustFieldName(f.name)}: into.${rustFieldName(f.name)}`).join(", ")} }`
376
+ : `into.val`;
377
+ parts.push(`#[allow(dead_code)]
378
+ fn ${fn}(over: &${overElemTy}, into: ${elemRowGo}) -> ${elemRef.name} {
379
+ ${elemRef.name} {
380
+ ${inits.join("\n")}
381
+ ${rustFieldName(intoKey)}: ${intoValue},
382
+ }
383
+ }`);
384
+ void intoField;
385
+ }
386
+ return parts.join("\n\n");
387
+ }
388
+ /** The Rust type of the over-element for a covered map node (parent collection element type). */
389
+ function mapOverElemType(comp, node, typedNodes, plan) {
390
+ const m = node.map;
391
+ const e = classifyExpr(m.over);
392
+ if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
393
+ const baseRef = typedNodes.get(e.path[0]);
394
+ const { ref } = typedFieldAccess(`${typedCell(e.path[0])}.borrow()`, baseRef, e.path.slice(1), plan);
395
+ if (ref.kind === "arr")
396
+ return renderTypeRef(ref.elem);
397
+ }
398
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined map (#86 pt2): map '${node.id}' 'over' must be a ref to a prior node's typed arr field.`);
399
+ }
400
+ // ── eligibility ─────────────────────────────────────────────────────────────────────
401
+ function reconstructR(e) {
402
+ switch (e.kind) {
403
+ case "str":
404
+ return e.value;
405
+ case "bool":
406
+ return e.value;
407
+ case "null":
408
+ return null;
409
+ case "ref":
410
+ return { [e.opt ? "refOpt" : "ref"]: e.path };
411
+ case "concat":
412
+ return { concat: e.parts.map(reconstructR) };
413
+ case "dynamic":
414
+ return e.node;
415
+ }
416
+ }
417
+ /**
418
+ * numLiteralRustExpr — a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
419
+ * `classifyExpr` leaves a bare number `dynamic` (its checked int/float range rules are the evaluate
420
+ * SSoT), so it never reaches the static str/bool/null branch — but a number literal IS statically
421
+ * resolvable to a native `Value`: the SAME range check the runtime applies (integral literal in the
422
+ * safe 2^53-1 window → Value::Int; a fractional/exponent literal → the checked Value::Float) is
423
+ * performed HERE at generation time. Returns the Rust expression for the literal's Value, or null
424
+ * when the node is not a bare number OR an integral literal is out of the safe window (keeping the
425
+ * runtime INVALID_LITERAL semantics — the port is not covered natively, the component falls through).
426
+ * Mirrors behavior_contracts eval of a bare number literal exactly.
427
+ */
428
+ const NUM_SAFE_INT_R = 9007199254740991; // 2^53 - 1 (expr safeInt)
429
+ function numLiteralRustExpr(node) {
430
+ if (typeof node !== "number" || !Number.isFinite(node))
431
+ return null;
432
+ if (Number.isInteger(node)) {
433
+ if (node < -NUM_SAFE_INT_R || node > NUM_SAFE_INT_R)
434
+ return null; // out of safe window → not covered
435
+ return `Value::Int(${node}i64)`;
436
+ }
437
+ // fractional / exponent literal → checked f64 (finite, guaranteed above).
438
+ return `Value::Float(${node}f64)`;
439
+ }
440
+ /** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
441
+ * resolvable to a native Value with no Obj / no evaluate interpreter. */
442
+ function portIsStatic(node) {
443
+ const arr = staticArrElems(node);
444
+ if (arr !== null)
445
+ return arr.every(portIsStatic);
446
+ if (numLiteralRustExpr(node) !== null)
447
+ return true; // bare number literal (e.g. Query `limit`)
448
+ const e = classifyExpr(node);
449
+ switch (e.kind) {
450
+ case "str":
451
+ case "bool":
452
+ case "null":
453
+ case "ref":
454
+ return true;
455
+ case "concat":
456
+ return e.parts.every((p) => portIsStatic(reconstructR(p)));
457
+ default:
458
+ return false;
459
+ }
460
+ }
461
+ /** The output must lower to a typed struct/value with no scope read: a ref to a BODY NODE, an
462
+ * obj/arr of such, or a concat. A ref to an input param / non-concat operator is not lowerable. */
463
+ function outputIsNativeLowerable(comp) {
464
+ const bodyIds = new Set(comp.body.map((n) => n.id));
465
+ const walk = (node) => {
466
+ const arr = staticArrElems(node);
467
+ if (arr !== null)
468
+ return arr.every(walk);
469
+ if (node !== null && typeof node === "object" && !Array.isArray(node)) {
470
+ const keys = Object.keys(node);
471
+ if (keys.length === 1 && keys[0] === "obj") {
472
+ const obj = node.obj;
473
+ return Object.keys(obj).every((k) => walk(obj[k]));
474
+ }
475
+ if (keys.length === 1 && keys[0] === "arr")
476
+ return true;
477
+ }
478
+ const e = classifyExpr(node);
479
+ if (e.kind === "str" || e.kind === "bool" || e.kind === "null")
480
+ return true;
481
+ if (e.kind === "ref")
482
+ return bodyIds.has(e.path[0]);
483
+ if (e.kind === "concat")
484
+ return e.parts.every((p) => walk(reconstructR(p)));
485
+ return false;
486
+ };
487
+ return walk(comp.output);
488
+ }
489
+ /**
490
+ * coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet), rust twin of the go predicate.
491
+ */
492
+ function coveredMapNode(n, bodyIds) {
493
+ if (n === null || typeof n !== "object" || !("map" in n))
494
+ return false;
495
+ const m = n.map;
496
+ if (m.when !== undefined)
497
+ return false;
498
+ if (m.policy !== undefined && m.policy !== "fail")
499
+ return false;
500
+ if (m.relationKind !== undefined && m.relationKind !== "single" && m.relationKind !== "connection")
501
+ return false;
502
+ const overE = classifyExpr(m.over);
503
+ if (overE.kind !== "ref" || !bodyIds.has(overE.path[0]))
504
+ return false;
505
+ const ports = m.ports;
506
+ for (const p of Object.keys(ports))
507
+ if (!portIsStatic(ports[p]))
508
+ return false;
509
+ return true;
510
+ }
511
+ /** A covered read SHAPE with static ports + native-lowerable output (+ covered map...into), OR a
512
+ * real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point reads. */
513
+ function isSequentialComponentRefRead(comp) {
514
+ return coverageRejectReason(comp) === null;
515
+ }
516
+ /**
517
+ * coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89), rust twin of
518
+ * the go predicate. Returns null when the component IS a covered native read; otherwise a SPECIFIC
519
+ * human-readable reason naming the exact disqualifier and the offending node id. The predicate logic is
520
+ * IDENTICAL to isSequentialComponentRefRead (defined as `coverageRejectReason(c) === null`) so the two
521
+ * can never disagree. This is the message the fail-closed dispatch surfaces per non-native component.
522
+ */
523
+ function coverageRejectReason(comp) {
524
+ const ep = execPlan(comp);
525
+ if (ep === null)
526
+ return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
527
+ for (const [i] of ep.parallelStageOf) {
528
+ const n = comp.body[i];
529
+ if (!("component" in n))
530
+ return `node '${n.id}' is a non-componentRef member of a parallel stage (not native-covered)`;
531
+ if (n.bindField !== undefined)
532
+ return `node '${n.id}' is a bindField relation child inside a parallel stage (not native-covered)`;
533
+ }
534
+ const bodyIds = new Set(comp.body.map((b) => b.id));
535
+ for (const n of comp.body) {
536
+ if ("cond" in n)
537
+ return `node '${n.id}' is a cond branch (not yet native-covered)`;
538
+ if ("map" in n) {
539
+ if (!coveredMapNode(n, bodyIds))
540
+ return `node '${n.id}' is a non-covered map shape (guarded/over-input)`;
541
+ continue;
542
+ }
543
+ if (!("component" in n))
544
+ return `node '${n.id}' is not a componentRef, map, or cond node (not native-covered)`;
545
+ const cr = n;
546
+ if (cr.policy !== undefined && cr.policy !== "fail")
547
+ return `node '${n.id}' componentRef policy '${cr.policy}' (produced-aware output not yet native-covered)`;
548
+ if (cr.bindField !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
549
+ return `node '${n.id}' relationKind '${cr.relationKind ?? "point"}' not native-covered (bindField only supports single|connection)`;
550
+ if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
551
+ return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
552
+ for (const p of Object.keys(cr.ports))
553
+ if (portIsStatic(cr.ports[p]) === false)
554
+ return `node '${n.id}' port '${p}' is not statically resolvable`;
555
+ }
556
+ if (!outputIsNativeLowerable(comp))
557
+ return "output is not natively lowerable";
558
+ return null;
559
+ }
560
+ function isFullyTyped(comp) {
561
+ if (comp.outputType === undefined)
562
+ return false;
563
+ for (const n of comp.body)
564
+ if (n.outType === undefined)
565
+ return false;
566
+ return true;
567
+ }
568
+ function isNativeRaw(comp) {
569
+ return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
570
+ }
571
+ function assertTypedCoverage(comp) {
572
+ if (!isSequentialComponentRefRead(comp))
573
+ return;
574
+ if (isFullyTyped(comp))
575
+ return;
576
+ 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).`);
577
+ }
578
+ // ── typed input struct (InNR<Comp>) + NATIVE scalar port lowering ────────────────────────────
579
+ /** Rust type for an input port's declared type (scalar → concrete; anything else → Value). */
580
+ function inputPortRustType(schema) {
581
+ switch (schema?.type) {
582
+ case "string":
583
+ return "String";
584
+ case "int":
585
+ return "i64";
586
+ case "float":
587
+ return "f64";
588
+ case "bool":
589
+ return "bool";
590
+ default:
591
+ return "Value";
592
+ }
593
+ }
594
+ /** The scalar kind an input port declares, or undefined if it is not a native scalar. */
595
+ function inputScalarKind(schema) {
596
+ switch (schema?.type) {
597
+ case "string":
598
+ return "String";
599
+ case "int":
600
+ return "i64";
601
+ case "float":
602
+ return "f64";
603
+ case "bool":
604
+ return "bool";
605
+ default:
606
+ return undefined;
607
+ }
608
+ }
609
+ /** emitInStruct — the concrete input struct declaration for a covered component (fields = inputPorts).
610
+ * Empty inputPorts => empty struct. Derives Default so a consumer/test can build it field-by-field. */
611
+ function emitInStruct(comp) {
612
+ const name = inStructName(comp.name);
613
+ const ports = Object.keys(comp.inputPorts ?? {});
614
+ if (ports.length === 0) {
615
+ return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\n#[derive(Default)]\npub struct ${name};`;
616
+ }
617
+ const fields = ports
618
+ .map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p])}, // ${JSON.stringify(p)}`)
619
+ .join("\n");
620
+ return `// ${name} — the CONCRETE input for '${comp.name}' (fields = inputPorts; typed, consumer-built —
621
+ // NO generic Value slice, NO per-field boxing crosses the covered read boundary).
622
+ #[derive(Default)]
623
+ pub struct ${name} {
624
+ ${fields}
625
+ }`;
626
+ }
627
+ /**
628
+ * emitNativeScalar — lower a static port expr to a NATIVE Rust expression of the concrete `want` scalar
629
+ * type, or null if it cannot be statically proven that type. A string literal is native for
630
+ * `want==="string"`, a bool literal for `want==="bool"`. A ref resolves via `resolveRef`. A concat lowers
631
+ * ONLY into `string` and only when EVERY part is a native string, emitting a single `format!("{}{}…", …)`.
632
+ */
633
+ function emitNativeScalar(node, want, resolveRef) {
634
+ if (staticArrElems(node) !== null)
635
+ return null;
636
+ const e = classifyExpr(node);
637
+ switch (e.kind) {
638
+ case "str":
639
+ return want === "String" ? `${rustStrLit(e.value)}.to_string()` : null;
640
+ case "bool":
641
+ return want === "bool" ? (e.value ? "true" : "false") : null;
642
+ case "null":
643
+ return null;
644
+ case "ref": {
645
+ const sc = resolveRef(e.path[0], e.path.slice(1), e.opt);
646
+ if (sc === null || sc.scalar !== want)
647
+ return null;
648
+ return sc.expr;
649
+ }
650
+ case "concat": {
651
+ if (want !== "String")
652
+ return null;
653
+ // a single-part concat is just the owned String of that part.
654
+ if (e.parts.length === 1)
655
+ return emitNativeScalar(reconstructR(e.parts[0]), "String", resolveRef);
656
+ // multi-part: fold into ONE format! (statically strings only — the concat's strings-only
657
+ // TYPE_MISMATCH can never fire, so dropping the check is sound). To stay clippy-clean
658
+ // (to_string_in_format_args), a string-literal part is baked into the format STRING itself
659
+ // (escaped) rather than passed as a `{}` arg; a ref part becomes a `{}` arg (Display on String).
660
+ let fmt = "";
661
+ const args = [];
662
+ for (const p of e.parts) {
663
+ const sub = classifyExpr(reconstructR(p));
664
+ if (sub.kind === "str") {
665
+ fmt += sub.value.replace(/[{}]/g, (c) => c + c); // escape { } for the format string
666
+ continue;
667
+ }
668
+ const arg = emitNativeScalar(reconstructR(p), "String", resolveRef);
669
+ if (arg === null)
670
+ return null;
671
+ fmt += "{}";
672
+ args.push(arg);
673
+ }
674
+ if (args.length === 0)
675
+ return `${rustStrLit(fmt)}.to_string()`;
676
+ return `format!(${rustStrLit(fmt)}, ${args.join(", ")})`;
677
+ }
678
+ default:
679
+ return null;
680
+ }
681
+ }
682
+ // bc#90 / runtime-free: the old `emitStaticR` / `inputLeafR` / `valueLeafR` port lowering (which built
683
+ // `Value` leaves via the primitives::* helpers and struct-field serializers) is REMOVED — every covered
684
+ // port now lowers to a FULLY NATIVE Rust value in emitPortInit (string / bool / static-string-array /
685
+ // number literal), and a genuinely-dynamic port fails closed. There is no boxed-Value fallback on the
686
+ // covered read plane.
687
+ /**
688
+ * emitPortInit — emit the port field build for one port as a FULLY NATIVE Rust expression (bc#90 /
689
+ * runtime-free). Every covered port lowers to a concrete Rust value assigned straight into the ports
690
+ * struct field — NO `Value`, NO fallback path, NO `match`:
691
+ * - a static string array (projection) → `vec!["a", "b", ...]` (change #2);
692
+ * - a bare int/float number literal (e.g. Query `limit: 20`) → `20i64` / `<n>f64`;
693
+ * - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
694
+ * A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered plane —
695
+ * a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
696
+ * ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
697
+ */
698
+ function emitPortInit(pn, expr, fieldRustType, resolveRef) {
699
+ const field = portFieldName(pn);
700
+ // change #2: a static string array (projection) → a native `vec![...]` of &'static str.
701
+ const strArr = staticStringArrElems(expr);
702
+ if (strArr !== null) {
703
+ if (fieldRustType !== "Vec<&'static str>") {
704
+ 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>).`);
705
+ }
706
+ return `${field}: vec![${strArr.map((s) => rustStrLit(s)).join(", ")}]`;
707
+ }
708
+ // a bare number literal (e.g. Query `limit: 20`): the SSoT range check ran in numLiteralRustExpr —
709
+ // emit the concrete native i64/f64 (unwrap the Value::Int/Float box: it was only there for the old
710
+ // boxed path; the range check is what matters, and it already passed).
711
+ const num = numLiteralRustExpr(expr);
712
+ if (num !== null) {
713
+ if (fieldRustType === "i64")
714
+ return `${field}: ${expr}i64`;
715
+ if (fieldRustType === "f64")
716
+ return `${field}: ${expr}f64`;
717
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90): port '${pn}' is a number literal but its field type is '${fieldRustType}'.`);
718
+ }
719
+ // string / bool / scalar-input / concat → native scalar expression (no Value, no `match`).
720
+ const native = emitNativeScalar(expr, fieldRustType, resolveRef);
721
+ if (native !== null) {
722
+ return `${field}: ${native}`;
723
+ }
724
+ // FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would re-couple
725
+ // the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the component is not
726
+ // native-eligible and must regenerate on the boxed straight-line path.
727
+ 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.`);
728
+ }
729
+ // ── the combined runner (STRUCT-returning; INLINE sequential; native ports in; concrete row out) ──
730
+ /** resolveRefAt for the top-level runner: a ref head is a PRIOR NODE (its typed scalar field) or an
731
+ * INPUT PORT (in_.<field>). A REQUIRED scalar resolves to an OWNED native Rust expression; opt / non-
732
+ * scalar / refOpt → null (fall back to the Value path). */
733
+ function runnerResolveRef(priorNodeAt, atPos, typedNodes, plan, inputPorts) {
734
+ return (head, restPath, opt) => {
735
+ if (opt)
736
+ return null;
737
+ if (priorNodeAt(head, atPos)) {
738
+ const baseRef = typedNodes.get(head);
739
+ let acc;
740
+ let expr;
741
+ try {
742
+ const r = typedFieldAccess(`${typedCell(head)}.borrow()`, baseRef, restPath, plan);
743
+ expr = r.expr;
744
+ acc = r.ref;
745
+ }
746
+ catch {
747
+ return null;
748
+ }
749
+ if (acc.kind !== "scalar" || acc.scalar === "null")
750
+ return null;
751
+ // OWNED: a string field is cloned; int/float/bool are Copy (deref off the Ref).
752
+ const owned = acc.scalar === "string" ? `(${expr}).clone()` : `*(${expr})`;
753
+ return { expr: owned, scalar: rustScalar(acc.scalar) };
754
+ }
755
+ if (restPath.length !== 0)
756
+ return null;
757
+ const scalar = inputScalarKind(inputPorts?.[head]);
758
+ if (scalar === undefined)
759
+ return null;
760
+ const field = `in_.${rustFieldName(head)}`;
761
+ const owned = scalar === "String" ? `${field}.clone()` : field;
762
+ return { expr: owned, scalar };
763
+ };
764
+ }
765
+ function rustScalar(s) {
766
+ return s === "string" ? "String" : s === "int" ? "i64" : s === "float" ? "f64" : "bool";
767
+ }
768
+ function emitNativeRawRunner(comp, plan, isAsync = false) {
769
+ const ep = execPlan(comp);
770
+ const order = ep.order;
771
+ const hasConcurrency = ep.parallelStageOf.size > 0;
772
+ const fn = sanitize(comp.name);
773
+ // bc#97 (uniform-rust-async slice): the async runner awaits each async handler at its call site. A
774
+ // real-concurrency stage (bc#87) dispatches handlers from scoped std threads — that mechanism cannot
775
+ // host `.await` (a std thread is not an async task), and mixing it with an async terminal handler is
776
+ // a per-node-propagation problem outside THIS slice. Fail closed LOUDLY rather than emit code that
777
+ // won't compile.
778
+ if (isAsync && hasConcurrency) {
779
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native async (bc#97): component '${comp.name}' has a bc#87 real-concurrency stage, which dispatches handlers on scoped std threads and cannot await async handlers. Async + concurrent-stage is a per-node-propagation follow-up (out of this uniform-async slice) — generate this component with io-model=sync, or split the concurrency stage.`);
780
+ }
781
+ const typedNodes = new Map();
782
+ for (const n of comp.body)
783
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
784
+ const idToIndex = new Map();
785
+ comp.body.forEach((n, i) => idToIndex.set(n.id, i));
786
+ const opsLiteral = comp.body.map((n) => {
787
+ const cr = n;
788
+ const pid = cr.parent;
789
+ return { id: n.id, parent: pid !== undefined ? idToIndex.get(pid) ?? null : null, policy: cr.policy, bindField: cr.bindField };
790
+ });
791
+ const metas = analyzeSequentialOps(opsLiteral, order);
792
+ const idToPos = new Map();
793
+ order.forEach((opIdx, k) => idToPos.set(comp.body[opIdx].id, k));
794
+ const priorNodeAt = (head, atPos) => {
795
+ const p = idToPos.get(head);
796
+ return p !== undefined && p < atPos;
797
+ };
798
+ const outRef = comp.outputType !== undefined ? deriveTypeRef(comp.outputType, plan) : undefined;
799
+ const outTy = outRef ? renderTypeRef(outRef) : "Value";
800
+ // Does any port read an input param? If not, `in_` is unused — bind it as `_in_` to stay clippy-clean.
801
+ const readsInput = (() => {
802
+ const walk = (node, atPos) => {
803
+ const arr = staticArrElems(node);
804
+ if (arr !== null)
805
+ return arr.some((el) => walk(el, atPos));
806
+ const e = classifyExpr(node);
807
+ if (e.kind === "ref")
808
+ return !priorNodeAt(e.path[0], atPos);
809
+ if (e.kind === "concat")
810
+ return e.parts.some((p) => walk(reconstructR(p), atPos));
811
+ return false;
812
+ };
813
+ return [...order.keys()].some((k) => {
814
+ const nd = comp.body[order[k]];
815
+ if ("map" in nd) {
816
+ const m = nd.map;
817
+ const asName = m.as;
818
+ const walkMap = (node) => {
819
+ const arr = staticArrElems(node);
820
+ if (arr !== null)
821
+ return arr.some(walkMap);
822
+ const e = classifyExpr(node);
823
+ if (e.kind === "ref")
824
+ return e.path[0] !== asName && !priorNodeAt(e.path[0], k);
825
+ if (e.kind === "concat")
826
+ return e.parts.some((p) => walkMap(reconstructR(p)));
827
+ return false;
828
+ };
829
+ return Object.values(m.ports).some((pn) => walkMap(pn));
830
+ }
831
+ const cr = nd;
832
+ return Object.values(cr.ports).some((pn) => walk(pn, k));
833
+ });
834
+ })();
835
+ const inParam = readsInput ? "in_" : "_in_";
836
+ const lines = [];
837
+ lines.push(`// run_native_raw_struct_${fn} — the STRUCT-RETURNING combined read (bc#77/#87/#94): the fully`);
838
+ lines.push(`// de-plumbed CONCRETE path. Generic over the per-component CONCRETE ${handlerTraitName(comp.name)} trait,`);
839
+ lines.push(`// whose node_* methods take the node's native ports struct and return the concrete per-node row`);
840
+ lines.push(`// struct (typed fields = the outType). The runner builds each ports struct by direct native`);
841
+ lines.push(`// construction (a simple string port lowers to a native Rust expr), dispatches the concrete`);
842
+ lines.push(`// node_* method, and reads row.<field> DIRECTLY into the node's outType struct cell — no boxed`);
843
+ lines.push(`// handler result, no generic enum crossing, no dispatch on a dynamic value on the covered plane. Node`);
844
+ lines.push(`// results are typed struct cells; a relation child reads the parent's REAL struct result via`);
845
+ lines.push(`// direct field access (child-present decision from the real parent value — relationSingle /`);
846
+ lines.push(`// connection converge). A real-concurrency stage (bc#87) is static parallel orchestration —`);
847
+ lines.push(`// scoped worker threads (bounded by the static plan.concurrency) call the concrete node_*`);
848
+ lines.push(`// method; preflight + interpret are committed in ascending index order so the value / op`);
849
+ lines.push(`// multiset / failure precedence byte-match run_behavior. The output is a typed struct/value`);
850
+ lines.push(`// assembled by struct literal + field access — the consumer keeps it native.`);
851
+ const bound = hasConcurrency ? `<H: ${handlerTraitName(comp.name)} + Sync>` : `<H: ${handlerTraitName(comp.name)}>`;
852
+ // bc#97: async propagation (uniform) — if the terminal handlers are async, every handler call awaits,
853
+ // so the runner is itself `async fn` and returns a future the (already-async) consumer awaits. Sync
854
+ // default emits `pub fn` exactly as before (byte-identical).
855
+ const fnKw = isAsync ? "pub async fn" : "pub fn";
856
+ lines.push(`${fnKw} run_native_raw_struct_${fn}${bound}(`);
857
+ lines.push(` handlers: &H,`);
858
+ lines.push(` ${inParam}: ${inStructName(comp.name)},`);
859
+ lines.push(`) -> Result<${outTy}, BehaviorError> {`);
860
+ for (const k of order.keys()) {
861
+ const id = comp.body[order[k]].id;
862
+ lines.push(` let ${typedCell(id)}: RefCell<${renderTypeRef(typedNodes.get(id))}> = RefCell::new(Default::default());`);
863
+ lines.push(` let produced_${sanitize(id)} = std::cell::Cell::new(false);`);
864
+ lines.push(` let _ = &produced_${sanitize(id)};`);
865
+ }
866
+ for (let k = 0; k < order.length; k++) {
867
+ const i = order[k];
868
+ const stage = ep.parallelStageOf.get(i);
869
+ if (stage !== undefined && stage[0] === i) {
870
+ lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt));
871
+ k += stage.length - 1;
872
+ continue;
873
+ }
874
+ if ("map" in comp.body[i]) {
875
+ lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, isAsync));
876
+ continue;
877
+ }
878
+ const node = comp.body[i];
879
+ const meta = metas[i];
880
+ const s = sanitize(node.id);
881
+ const structName = portsStructName(comp.name, node.id);
882
+ const portNames = Object.keys(node.ports);
883
+ const ref = typedNodes.get(node.id);
884
+ lines.push(` // ── op '${node.id}' (${node.component}${node.relationKind ? ", relationKind:" + node.relationKind : ""}${node.parent ? ", parent:" + node.parent : ""}) ──`);
885
+ let indent = " ";
886
+ const closers = [];
887
+ if (node.parent !== undefined && priorNodeAt(node.parent, k)) {
888
+ lines.push(`${indent}if produced_${sanitize(node.parent)}.get() {`);
889
+ closers.unshift(`${indent}}`);
890
+ indent += " ";
891
+ if (node.bindField !== undefined) {
892
+ // bind (change #3 — typed produced-aware bound): read the parent struct field DIRECTLY (typed
893
+ // access — no serialize). `bound_<s>` is a typed `Option<K>` — None => UNPRODUCED/skip, Some =>
894
+ // produced (an empty string is a VALID produced value, `Some("")`). This is the EXACT `!= Null`
895
+ // skip-gate ≡ run_behavior's preflightOp, now runtime-free (NO Value).
896
+ const parentRef = typedNodes.get(node.parent);
897
+ const { expr: bfExpr, ref: bfRef } = typedFieldAccess(`${typedCell(node.parent)}.borrow()`, parentRef, [node.bindField], plan);
898
+ if (bfRef.kind === "opt") {
899
+ // an opt bindField is ALREADY an Option<T>: None => skip, Some => produced. Clone it directly.
900
+ lines.push(`${indent}let bound_${s}: ${boundRustType(node, typedNodes, plan)} = ${bfExpr}.clone();`);
901
+ }
902
+ else {
903
+ // a required scalar bindField is always PRESENT (never skips on null): wrap in Some so the
904
+ // produced Option is never None, preserving the "produced" semantics of the old guard.
905
+ const owned = bfRef.kind === "scalar" && bfRef.scalar === "string" ? `(${bfExpr}).clone()` : `*(${bfExpr})`;
906
+ lines.push(`${indent}let bound_${s}: ${boundRustType(node, typedNodes, plan)} = Some(${owned});`);
907
+ }
908
+ lines.push(`${indent}if bound_${s}.is_some() {`);
909
+ closers.unshift(`${indent}}`);
910
+ indent += " ";
911
+ }
912
+ }
913
+ const resolveRef = runnerResolveRef(priorNodeAt, k, typedNodes, plan, comp.inputPorts);
914
+ const inits = [];
915
+ for (const pn of portNames) {
916
+ const expr = node.ports[pn];
917
+ const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`);
918
+ inits.push(emitPortInit(pn, expr, ty, resolveRef));
919
+ }
920
+ const boundArg = node.bindField !== undefined ? `bound_${s}` : "None";
921
+ const awaitSfx = isAsync ? ".await" : "";
922
+ lines.push(`${indent}let ports_${s} = ${structName} { ${inits.join(", ")} };`);
923
+ lines.push(`${indent}let row_${s} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
924
+ lines.push(`${indent} Some(r) => r,`);
925
+ lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(node.component)})),`);
926
+ lines.push(`${indent}};`);
927
+ if (meta.policy === "continue") {
928
+ lines.push(`${indent}if !row_${s}.is_error {`);
929
+ lines.push(`${indent} ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
930
+ lines.push(`${indent} produced_${s}.set(true);`);
931
+ lines.push(`${indent}}`);
932
+ }
933
+ else {
934
+ const label = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
935
+ lines.push(`${indent}if row_${s}.is_error {`);
936
+ lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under '${label}: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
937
+ lines.push(`${indent}}`);
938
+ lines.push(`${indent}${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
939
+ lines.push(`${indent}produced_${s}.set(true);`);
940
+ }
941
+ for (const c of closers)
942
+ lines.push(c);
943
+ }
944
+ // output: assemble a TYPED struct/value (struct literal + typed field access — ZERO Value::Obj).
945
+ const skippable = new Set();
946
+ const skippableKind = new Map();
947
+ for (const k of order.keys()) {
948
+ if (metas[order[k]].canSkip) {
949
+ const bodyNode = comp.body[order[k]];
950
+ skippable.add(bodyNode.id);
951
+ const rk = "map" in bodyNode ? bodyNode.map.relationKind : bodyNode.relationKind;
952
+ skippableKind.set(bodyNode.id, rk);
953
+ }
954
+ }
955
+ const out = skippable.size > 0
956
+ ? emitProducedAwareValue(comp.output, outRef, typedNodes, plan, skippable, skippableKind)
957
+ : emitTypedValue(comp.output, outRef, typedNodes, plan);
958
+ lines.push(` let __out = ${out.expr};`);
959
+ lines.push(` Ok(__out)`);
960
+ lines.push(`}`);
961
+ return lines.join("\n");
962
+ }
963
+ /**
964
+ * emitParallelStageArm (bc#87/#94, rust CONCRETE) — EXPLICIT static parallel orchestration for a real-
965
+ * concurrency stage. Reproduces run_plan_parallel's deterministic protocol so the observed value / op
966
+ * multiset / failure precedence byte-match run_behavior:
967
+ * 1. PREFLIGHT (ascending): parent-produced gate + build each member's CONCRETE ports struct.
968
+ * 2. DISPATCH (bounded scoped threads): share `&H` (the concrete trait; `H: Sync`), pull runnable
969
+ * members from a shared atomic cursor in ascending order, call the concrete node_* method. Each
970
+ * concrete row lands in a per-member Mutex slot (None = unknown component).
971
+ * 3. COMMIT (ascending index): interpret policy + move the concrete row into the outType cell —
972
+ * lowest-index failure wins.
973
+ */
974
+ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt) {
975
+ const lines = [];
976
+ const ids = stage.map((i) => comp.body[i].id);
977
+ lines.push(` // ── bc#87 real-concurrency stage {${ids.map((x) => rustStrLit(x)).join(", ")}} — static parallel`);
978
+ lines.push(` // orchestration (scoped threads, bound=${concurrency}); preflight+commit in ascending index order.`);
979
+ lines.push(` {`);
980
+ // 1) PREFLIGHT (ascending): build each member's CONCRETE ports struct into a stage-scoped Option local.
981
+ for (const i of stage) {
982
+ const node = comp.body[i];
983
+ const s = sanitize(node.id);
984
+ const structName = portsStructName(comp.name, node.id);
985
+ const gated = node.parent !== undefined && priorNodeAt(node.parent, atPos);
986
+ let ind = " ";
987
+ const closers = [];
988
+ if (gated) {
989
+ lines.push(` let mut ports_${s}: Option<${structName}> = None;`);
990
+ lines.push(`${ind}if produced_${sanitize(node.parent)}.get() {`);
991
+ closers.unshift(`${ind}}`);
992
+ ind += " ";
993
+ }
994
+ const resolveRef = runnerResolveRef(priorNodeAt, atPos, typedNodes, plan, comp.inputPorts);
995
+ const inits = [];
996
+ for (const pn of Object.keys(node.ports)) {
997
+ const expr = node.ports[pn];
998
+ const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`);
999
+ inits.push(emitPortInit(pn, expr, ty, resolveRef));
1000
+ }
1001
+ if (gated) {
1002
+ lines.push(`${ind}ports_${s} = Some(${structName} { ${inits.join(", ")} });`);
1003
+ }
1004
+ else {
1005
+ lines.push(`${ind}let ports_${s}: Option<${structName}> = Some(${structName} { ${inits.join(", ")} });`);
1006
+ }
1007
+ for (const c of closers)
1008
+ lines.push(c);
1009
+ }
1010
+ // 2) DISPATCH (bounded scoped threads). Share `&H` (concrete trait; H: Sync). Each worker calls the
1011
+ // concrete node_* method for its member's CONCRETE ports and stores the concrete row in its slot.
1012
+ const nMembers = stage.length;
1013
+ const boundN = Math.min(concurrency, nMembers);
1014
+ const jobsVar = `jobs_${sanitize(comp.name)}`;
1015
+ for (const i of stage) {
1016
+ const s = sanitize(comp.body[i].id);
1017
+ const rowT = rawRowStructName(comp.name, comp.body[i].id);
1018
+ lines.push(` let slot_${s}: std::sync::Mutex<Option<Option<${rowT}>>> = std::sync::Mutex::new(None);`);
1019
+ }
1020
+ lines.push(` let mut ${jobsVar}: Vec<usize> = Vec::new();`);
1021
+ stage.forEach((i, slot) => {
1022
+ const s = sanitize(comp.body[i].id);
1023
+ lines.push(` if ports_${s}.is_some() { ${jobsVar}.push(${slot}); }`);
1024
+ });
1025
+ lines.push(` let cursor = std::sync::atomic::AtomicUsize::new(0);`);
1026
+ lines.push(` let workers = ${boundN}usize.min(${jobsVar}.len());`);
1027
+ lines.push(` std::thread::scope(|scope| {`);
1028
+ lines.push(` for _ in 0..workers {`);
1029
+ lines.push(` scope.spawn(|| loop {`);
1030
+ lines.push(` let j = cursor.fetch_add(1, std::sync::atomic::Ordering::SeqCst);`);
1031
+ lines.push(` if j >= ${jobsVar}.len() { break; }`);
1032
+ lines.push(` let slot = ${jobsVar}[j];`);
1033
+ lines.push(` match slot {`);
1034
+ stage.forEach((i, slot) => {
1035
+ const node = comp.body[i];
1036
+ const s = sanitize(node.id);
1037
+ lines.push(` ${slot} => {`);
1038
+ lines.push(` let ports = ports_${s}.as_ref().expect("runnable member has ports");`);
1039
+ lines.push(` let row = handlers.${nodeMethodName(node.id)}(ports, None);`);
1040
+ lines.push(` *slot_${s}.lock().unwrap() = Some(row);`);
1041
+ lines.push(` }`);
1042
+ });
1043
+ lines.push(` _ => unreachable!(),`);
1044
+ lines.push(` };`);
1045
+ lines.push(` });`);
1046
+ lines.push(` }`);
1047
+ lines.push(` });`);
1048
+ // 3) COMMIT (ascending index): interpret + move the concrete row into the outType cell.
1049
+ stage.forEach((i) => {
1050
+ const node = comp.body[i];
1051
+ const meta = metas[i];
1052
+ const s = sanitize(node.id);
1053
+ const ref = typedNodes.get(node.id);
1054
+ lines.push(` if ports_${s}.is_some() {`);
1055
+ lines.push(` let row_${s} = match slot_${s}.lock().unwrap().take() {`);
1056
+ lines.push(` Some(Some(r)) => r,`);
1057
+ lines.push(` _ => return Err(unknown_component(${rustStrLit(node.component)})),`);
1058
+ lines.push(` };`);
1059
+ if (meta.policy === "continue") {
1060
+ lines.push(` if !row_${s}.is_error {`);
1061
+ lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1062
+ lines.push(` produced_${s}.set(true);`);
1063
+ lines.push(` }`);
1064
+ }
1065
+ else {
1066
+ const label = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1067
+ lines.push(` if row_${s}.is_error {`);
1068
+ lines.push(` return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under '${label}: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
1069
+ lines.push(` }`);
1070
+ lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1071
+ lines.push(` produced_${s}.set(true);`);
1072
+ }
1073
+ lines.push(` }`);
1074
+ });
1075
+ lines.push(` }`);
1076
+ return lines.join("\n");
1077
+ }
1078
+ // bc#90 / runtime-free: the old `emitMapStaticR` map-element port lowering (a Value fallback) is REMOVED
1079
+ // — covered map element ports lower to a fully native Rust value in emitPortInit (the map resolveRef
1080
+ // reads the $as element / prior-node / input typed fields directly), with a fail-closed on a dynamic port.
1081
+ /**
1082
+ * emitMapArm — the struct-native exec of a covered map node (rust CONCRETE). Three shapes:
1083
+ * - #86 pt2: BATCHED map...into — dispatch once (concrete batch row), augmented-element materializer.
1084
+ * - #93 shape 2: NON-batched map...into — dispatch ONCE PER ELEMENT (N physical requests), into per element.
1085
+ * - #93 shape 3: BATCHED map NO into — dispatch once, collect each element from the concrete element row.
1086
+ */
1087
+ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync = false) {
1088
+ const m = node.map;
1089
+ const awaitSfx = isAsync ? ".await" : "";
1090
+ const batched = m.batched === true;
1091
+ const hasInto = m.into !== undefined;
1092
+ const s = sanitize(node.id);
1093
+ const structName = portsStructName(comp.name, node.id);
1094
+ const batch = `${structName}Batch`;
1095
+ const overE = classifyExpr(m.over);
1096
+ const overParent = overE.kind === "ref" ? overE.path[0] : "";
1097
+ const overBaseRef = typedNodes.get(overParent);
1098
+ const { ref: overRef } = typedFieldAccess(`${typedCell(overParent)}.borrow()`, overBaseRef, overE.kind === "ref" ? overE.path.slice(1) : [], plan);
1099
+ const overElemRef = overRef.elem;
1100
+ const overFieldPath = overE.kind === "ref" ? overE.path.slice(1).map((p) => `.${rustFieldName(p)}`).join("") : "";
1101
+ const elemMat = mapElemMaterializerName(node.id);
1102
+ const priorHere = (head) => priorNodeAt(head, atPos);
1103
+ const asName = m.as;
1104
+ const portNames = Object.keys(m.ports);
1105
+ const elemArrRef = typedNodes.get(node.id);
1106
+ const elemName = elemArrRef.elem.name;
1107
+ const resolveRef = (head, restPath, opt) => {
1108
+ if (opt)
1109
+ return null;
1110
+ let baseExpr;
1111
+ let baseRef;
1112
+ if (head === asName) {
1113
+ baseExpr = `oel_${s}`;
1114
+ baseRef = overElemRef;
1115
+ }
1116
+ else if (priorHere(head)) {
1117
+ baseExpr = `${typedCell(head)}.borrow()`;
1118
+ baseRef = typedNodes.get(head);
1119
+ }
1120
+ else {
1121
+ if (restPath.length !== 0)
1122
+ return null;
1123
+ const scalar = inputScalarKind(comp.inputPorts?.[head]);
1124
+ if (scalar === undefined)
1125
+ return null;
1126
+ const field = `in_.${rustFieldName(head)}`;
1127
+ return { expr: scalar === "String" ? `${field}.clone()` : field, scalar };
1128
+ }
1129
+ let acc;
1130
+ let expr;
1131
+ try {
1132
+ const r = typedFieldAccess(baseExpr, baseRef, restPath, plan);
1133
+ expr = r.expr;
1134
+ acc = r.ref;
1135
+ }
1136
+ catch {
1137
+ return null;
1138
+ }
1139
+ if (acc.kind !== "scalar" || acc.scalar === "null")
1140
+ return null;
1141
+ const owned = acc.scalar === "string" ? `(${expr}).clone()` : `*(${expr})`;
1142
+ return { expr: owned, scalar: rustScalar(acc.scalar) };
1143
+ };
1144
+ // build the per-element CONCRETE native ports struct. `il` = indent inside the element loop.
1145
+ const buildPorts = (il) => {
1146
+ const out = [];
1147
+ const inits = [];
1148
+ for (const pn of portNames) {
1149
+ const ty = portFieldRustType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`);
1150
+ inits.push(emitPortInit(pn, m.ports[pn], ty, resolveRef));
1151
+ }
1152
+ out.push(`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`);
1153
+ return out;
1154
+ };
1155
+ const lines = [];
1156
+ const closers = [];
1157
+ lines.push(` // ── map '${node.id}' (${m.component}, ${batched ? "batched" : "per-element"}, into:${JSON.stringify(m.into ?? null)}${m.relationKind ? ", relationKind:" + m.relationKind : ""}${m.parent ? ", parent:" + m.parent : ""}) ──`);
1158
+ let indent = " ";
1159
+ if (m.parent !== undefined && priorNodeAt(m.parent, atPos)) {
1160
+ lines.push(`${indent}if produced_${sanitize(m.parent)}.get() {`);
1161
+ closers.unshift(`${indent}}`);
1162
+ indent += " ";
1163
+ }
1164
+ lines.push(`${indent}let over_${s} = ${typedCell(overParent)}.borrow()${overFieldPath}.clone();`);
1165
+ lines.push(`${indent}let mut built_${s}: Vec<${elemName}> = Vec::with_capacity(over_${s}.len());`);
1166
+ if (batched) {
1167
+ lines.push(`${indent}let mut items_${s}: Vec<${structName}> = Vec::with_capacity(over_${s}.len());`);
1168
+ lines.push(`${indent}for oel_${s} in over_${s}.iter() {`);
1169
+ for (const l of buildPorts(indent + " "))
1170
+ lines.push(l);
1171
+ lines.push(`${indent} items_${s}.push(ep_${s});`);
1172
+ lines.push(`${indent}}`);
1173
+ lines.push(`${indent}if !items_${s}.is_empty() {`);
1174
+ lines.push(`${indent} let want_${s} = items_${s}.len();`);
1175
+ lines.push(`${indent} let bp_${s} = ${batch} { items: items_${s} };`);
1176
+ lines.push(`${indent} let row_${s} = match handlers.${nodeMethodName(node.id)}(&bp_${s}, None)${awaitSfx} {`);
1177
+ lines.push(`${indent} Some(r) => r,`);
1178
+ lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(m.component)})),`);
1179
+ lines.push(`${indent} };`);
1180
+ lines.push(`${indent} if row_${s}.is_error {`);
1181
+ lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
1182
+ lines.push(`${indent} }`);
1183
+ lines.push(`${indent} if row_${s}.rows.len() != want_${s} {`);
1184
+ 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})));`);
1185
+ lines.push(`${indent} }`);
1186
+ lines.push(`${indent} let mut rows_iter_${s} = row_${s}.rows.into_iter();`);
1187
+ lines.push(`${indent} for over_el_${s} in over_${s}.iter() {`);
1188
+ lines.push(`${indent} let er_${s} = rows_iter_${s}.next().expect("aligned batch row");`);
1189
+ if (hasInto) {
1190
+ lines.push(`${indent} built_${s}.push(${elemMat}(over_el_${s}, er_${s}));`);
1191
+ }
1192
+ else {
1193
+ lines.push(`${indent} let _ = &over_el_${s};`);
1194
+ lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
1195
+ lines.push(`${indent} built_${s}.push(el_${s});`);
1196
+ }
1197
+ lines.push(`${indent} }`);
1198
+ lines.push(`${indent}}`);
1199
+ }
1200
+ else {
1201
+ // #93 shape 2: NON-batched map...into — dispatch ONCE PER ELEMENT.
1202
+ lines.push(`${indent}for oel_${s} in over_${s}.iter() {`);
1203
+ for (const l of buildPorts(indent + " "))
1204
+ lines.push(l);
1205
+ lines.push(`${indent} let er_${s} = match handlers.${nodeMethodName(node.id)}(&ep_${s}, None)${awaitSfx} {`);
1206
+ lines.push(`${indent} Some(r) => r,`);
1207
+ lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(m.component)})),`);
1208
+ lines.push(`${indent} };`);
1209
+ lines.push(`${indent} if er_${s}.is_error {`);
1210
+ lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(node.id)}, er_${s}.err)));`);
1211
+ lines.push(`${indent} }`);
1212
+ if (hasInto) {
1213
+ lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s}, er_${s}));`);
1214
+ }
1215
+ else {
1216
+ lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
1217
+ lines.push(`${indent} built_${s}.push(el_${s});`);
1218
+ }
1219
+ lines.push(`${indent}}`);
1220
+ }
1221
+ lines.push(`${indent}*${typedCell(node.id)}.borrow_mut() = built_${s};`);
1222
+ lines.push(`${indent}produced_${sanitize(node.id)}.set(true);`);
1223
+ for (const c of closers)
1224
+ lines.push(c);
1225
+ return lines.join("\n");
1226
+ }
1227
+ /** emitElemMove — move a concrete element row (RawElemNR, error already handled) into a fresh `let` local
1228
+ * of the element outType. Named → struct literal from moved fields; scalar/arr/opt → .val. */
1229
+ function emitElemMove(letBind, rowExpr, ref, plan) {
1230
+ if (ref.kind === "named") {
1231
+ const decl = findDecl(plan, ref.name);
1232
+ const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: ${rowExpr}.${rustFieldName(f.name)}`).join(", ");
1233
+ return `${letBind} = ${ref.name} { ${inits} };`;
1234
+ }
1235
+ return `${letBind} = ${rowExpr}.val;`;
1236
+ }
1237
+ /**
1238
+ * emitProducedAwareValue — the #86 part 1 produced-aware output lowering (rust twin of the go one).
1239
+ */
1240
+ function emitProducedAwareValue(node, expected, typedNodes, plan, skippable, skippableKind) {
1241
+ const e = classifyExpr(node);
1242
+ if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
1243
+ const head = e.path[0];
1244
+ const baseRef = typedNodes.get(head);
1245
+ if (skippable.has(head)) {
1246
+ const base = `${typedCell(head)}.borrow().clone()`;
1247
+ const { expr, ref } = typedFieldAccess(base, baseRef, e.path.slice(1), plan);
1248
+ const producedExpr = valueLeafProduced(expr, ref, expected);
1249
+ const isConn = skippableKind.get(head) === "connection" && e.path.length === 1;
1250
+ let unproduced;
1251
+ if (isConn) {
1252
+ const connTy = expected && expected.kind === "opt" ? renderTypeRef(expected.inner) : renderTypeRef(expected ?? ref);
1253
+ unproduced = expected && expected.kind === "opt" ? `Some(${connTy}::default())` : `${connTy}::default()`;
1254
+ }
1255
+ else {
1256
+ unproduced = expected && expected.kind === "opt" ? "None" : "Default::default()";
1257
+ }
1258
+ return {
1259
+ expr: `if produced_${sanitize(head)}.get() { ${producedExpr} } else { ${unproduced} }`,
1260
+ ref: expected ?? ref,
1261
+ };
1262
+ }
1263
+ const isBareCopyScalar = e.path.length === 1 && baseRef.kind === "scalar" && (baseRef.scalar === "int" || baseRef.scalar === "float" || baseRef.scalar === "bool");
1264
+ const base = isBareCopyScalar ? `*${typedCell(head)}.borrow()` : `${typedCell(head)}.borrow().clone()`;
1265
+ const { expr, ref } = typedFieldAccess(base, baseRef, e.path.slice(1), plan);
1266
+ return { expr, ref };
1267
+ }
1268
+ if (node !== null && typeof node === "object" && !Array.isArray(node)) {
1269
+ const keys = Object.keys(node);
1270
+ if (keys.length === 1 && keys[0] === "obj" && expected && expected.kind === "named") {
1271
+ const decl = findDecl(plan, expected.name);
1272
+ const fields = node.obj;
1273
+ const inits = decl.fields.map((f) => {
1274
+ const sub = emitProducedAwareValue(fields[f.name], f.type, typedNodes, plan, skippable, skippableKind);
1275
+ return `${rustFieldName(f.name)}: ${sub.expr}`;
1276
+ });
1277
+ return { expr: `${expected.name} { ${inits.join(", ")} }`, ref: expected };
1278
+ }
1279
+ }
1280
+ return emitTypedValue(node, expected, typedNodes, plan);
1281
+ }
1282
+ function valueLeafProduced(expr, ref, expected) {
1283
+ if (expected && expected.kind === "opt") {
1284
+ if (ref.kind === "opt")
1285
+ return expr;
1286
+ return `Some(${expr})`;
1287
+ }
1288
+ return expr;
1289
+ }
1290
+ // bc#90 / runtime-free: the old `valueLeafInner` (which serialized a bindField to a boxed `Value` for the
1291
+ // binding check) is REMOVED — the bound value is now a typed `Option<K>` built by direct field access
1292
+ // (Some(field.clone()) / the opt field's Option), and the produced gate is `Option::is_some()`.
1293
+ // ── struct dispatch surface (STRUCT-only — no Value bind, no serializer) ─────────────────
1294
+ function emitStructSurface(native) {
1295
+ const names = native.map((c) => rustStrLit(c.name));
1296
+ return `// COMPONENT_NAMES_NATIVE_RAW — covered reads exposed on the combined struct-native path. Each is
1297
+ // driven via run_native_raw_struct_<comp>(handlers, in_) -> Result<T, BehaviorError>: a STRUCT return
1298
+ // (the consumer builds the CONCRETE InNR_<comp> input + implements the CONCRETE HandlerNR<comp> trait).
1299
+ // See INTEGRATION.md §6.
1300
+ pub const COMPONENT_NAMES_NATIVE_RAW: [&str; ${names.length}] = [${names.join(", ")}];`;
1301
+ }
1302
+ // ── unknown_component helper (self-contained; the module carries no straight-line body) ──
1303
+ const UNKNOWN_COMPONENT_HELPER = `#[allow(dead_code)]
1304
+ fn unknown_component(component: &str) -> BehaviorError {
1305
+ BehaviorError::new("UNKNOWN_COMPONENT", format!("component '{component}' has no handler (fail-closed)"))
1306
+ }`;
1307
+ // ── module assembly ──────────────────────────────────────────────────────────────────
1308
+ function emit(ctx) {
1309
+ // bc#97: uniform I/O model for THIS generation — async terminal handlers ⇒ async fn + .await.
1310
+ const isAsync = ctx.ioModel === "async";
1311
+ for (const c of ctx.ir.components)
1312
+ assertTypedCoverage(c);
1313
+ const native = ctx.ir.components.filter(isNativeRaw);
1314
+ const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
1315
+ // FAIL CLOSED (bc#86/#89): the --typed-native (native codegen) surface is native-only. Previously a
1316
+ // non-covered component was SILENTLY DROPPED (emitted only as a comment note), concealing that it
1317
+ // never got native codegen. We now throw a LOUD GeneratorFailure naming every uncovered component +
1318
+ // its specific reject reason. A fully-covered module is unchanged.
1319
+ if (nonNative.length > 0) {
1320
+ const rejects = nonNative
1321
+ .map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
1322
+ .join("\n");
1323
+ 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` +
1324
+ `The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
1325
+ `box uncovered shapes. Implement native coverage for this shape, fix the spec if the shape is ` +
1326
+ `genuinely runtime-dynamic, or generate this component on the --ir dynamic interpreter surface.`);
1327
+ }
1328
+ if (native.length === 0) {
1329
+ // an empty native surface (no covered read) carries no runner and no failure — nothing references
1330
+ // bc-runtime, so it stays runtime-free (bc#90) with only the component-names const.
1331
+ return `#![allow(dead_code, unused_imports)]
1332
+ ${emitStructSurface([])}
1333
+ `;
1334
+ }
1335
+ const plan = buildTypePlan(ctx.ir);
1336
+ // The typed-native module is a de-boxed CONSUMER artifact: the covered runner RETURNS these outType
1337
+ // structs and the consumer reads their fields natively. So the decls (and their FIELDS) are pub — the
1338
+ // consumer keeps the model native (mirrors Go's exported outType struct fields on the covered path).
1339
+ const decls = emitTypeDecls(plan)
1340
+ .replace(/\nstruct /g, "\npub struct ")
1341
+ .replace(/^struct /gm, "pub struct ")
1342
+ .replace(/^( {4})([a-z_][A-Za-z0-9_]*: )/gm, "$1pub $2")
1343
+ .replace(/^( {4})(r#[a-z_][A-Za-z0-9_]*: )/gm, "$1pub $2");
1344
+ const defaults = emitDefaults(plan);
1345
+ const structs = [];
1346
+ const rowStructs = [];
1347
+ const handlerTraits = [];
1348
+ const inStructs = [];
1349
+ const elemMaterializers = [];
1350
+ for (const c of native) {
1351
+ const typedNodes = new Map();
1352
+ for (const n of c.body) {
1353
+ if ("map" in n)
1354
+ structs.push(emitMapPortsStructs(c, n));
1355
+ else
1356
+ structs.push(emitPortsStruct(c, n));
1357
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1358
+ }
1359
+ rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
1360
+ handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, isAsync));
1361
+ inStructs.push(emitInStruct(c));
1362
+ const mm = emitMapElemMaterializers(c, typedNodes, plan);
1363
+ if (mm)
1364
+ elemMaterializers.push(mm);
1365
+ }
1366
+ const runners = native.map((c) => emitNativeRawRunner(c, plan, isAsync)).join("\n\n");
1367
+ const structSurface = emitStructSurface(native);
1368
+ // (nonNative.length === 0 is guaranteed here — the fail-closed guard above threw otherwise.)
1369
+ const banner = `#![allow(dead_code, unused_imports, non_snake_case)]
1370
+ // GENERATED by behavior-contracts (bc#90, rust-typed-native — the RUNTIME-FREE read de-box; STRUCT-ONLY,
1371
+ // FULLY-NATIVE). A covered read is a strictly-sequential typed componentRef chain (point reads +
1372
+ // relationKind:single children), emitted ONLY as a STRUCT-returning runner GENERIC over a per-component
1373
+ // CONCRETE handler trait (HandlerNR<comp>): native ports struct IN (direct construction — every port is a
1374
+ // CONCRETE Rust value: String / bool / Vec<&str> projection / i64 limit — NO boxed Value, NO by-name
1375
+ // accessor); the handler takes a typed produced-aware 'bound' Option and returns the node's CONCRETE row
1376
+ // struct (typed fields = the projected outType), and the runner reads row.<field> DIRECTLY into the node's
1377
+ // outType struct. The covered plane carries NO bc-runtime reference at all — failures use the LOCAL
1378
+ // BehaviorError (same codes, byte-equal to run_behavior). INLINE sequential (no plan driver) so a relation
1379
+ // child reads the parent's REAL struct result via direct field access and the child-present decision is
1380
+ // made from the real parent value — relationSingle / connection converge with run_behavior (fixes
1381
+ // #323/#74). The exposed API is run_native_raw_struct_<comp>(handlers, in_) over the CONCRETE InNR_<comp>
1382
+ // input struct + the CONCRETE HandlerNR<comp> trait (the consumer builds/implements both, keeping the
1383
+ // model native). This module is FULLY NATIVE: a naive grep for boxing primitives OR for the bc-runtime
1384
+ // crate finds nothing, by design (bc#90 / runtime-free).
1385
+ use std::cell::RefCell;`;
1386
+ const sections = [
1387
+ banner,
1388
+ `// 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()}`,
1389
+ decls || undefined,
1390
+ defaults || undefined,
1391
+ UNKNOWN_COMPONENT_HELPER,
1392
+ `// Native ports structs (one per componentRef node; typed per the static port type — CONCRETE).\n${structs.join("\n\n")}`,
1393
+ `// CONCRETE per-node row structs (typed fields = the node's outType; + error signal).\n${rowStructs.join("\n\n")}`,
1394
+ `// CONCRETE per-component input structs (fields = inputPorts).\n${inStructs.join("\n\n")}`,
1395
+ `// CONCRETE per-component handler traits (one node_* method per node — native ports IN, row OUT).\n${handlerTraits.join("\n\n")}`,
1396
+ elemMaterializers.length
1397
+ ? `// map augmented-element copiers (pure struct field assignment — no dynamic value read).\n${elemMaterializers.join("\n\n")}`
1398
+ : undefined,
1399
+ `// Combined read runners (STRUCT-returning — the fully de-plumbed CONCRETE path).\n${runners}`,
1400
+ structSurface,
1401
+ ];
1402
+ return `${sections.filter((x) => !!x).join("\n\n")}\n`;
1403
+ }
1404
+ /**
1405
+ * rustTypedNativeObserve — the TEST-ONLY observe companion (a submodule string the test harness writes
1406
+ * alongside the covered module). It carries the typed->Value serializers (ser_*) + a CONCRETE scripted
1407
+ * handler (ScriptedNativeRaw) that implements every HandlerNR<comp> trait by decoding a scripted Value
1408
+ * into the concrete per-node row, + an observe_native_raw dispatch that decodes the generic input into the
1409
+ * concrete InNR_<comp>, drives the module-private struct runner, and serializes its result to a canonical
1410
+ * Value for the equivalence pin. Lives OUTSIDE the covered module so the module stays literal whole-file
1411
+ * zero (the ser_* / decode helpers are test glue, never on the consumer's read hot path).
1412
+ */
1413
+ export function rustTypedNativeObserve(ir) {
1414
+ const components = ir.components;
1415
+ const native = components.filter(isNativeRaw);
1416
+ const plan = buildTypePlan(ir);
1417
+ const needSync = native.some((c) => concurrencyPlan(c) !== null);
1418
+ const serializers = emitSerializers(plan);
1419
+ const marshallers = emitMarshallers(plan);
1420
+ // ── per-component input decoders: generic &[(String,Value)] -> concrete InNR_<comp> (test glue). ──
1421
+ const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
1422
+ // ── concrete scripted handler: implements every HandlerNR<comp> trait by decoding a scripted Value
1423
+ // into the concrete per-node row (the generic scripted-source queue meets the concrete seam). ──
1424
+ const decodeFns = [];
1425
+ const emittedDecode = new Set();
1426
+ const traitImpls = [];
1427
+ for (const c of native) {
1428
+ const typedNodes = new Map();
1429
+ for (const n of c.body)
1430
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1431
+ const methods = [];
1432
+ for (const n of c.body) {
1433
+ const ref = typedNodes.get(n.id);
1434
+ // bc#90 / runtime-free: the scripted impl's `_bound` param mirrors the trait's typed produced-aware
1435
+ // Option<K> (NOT a boxed Value) — computed per node so the impl signature matches the trait.
1436
+ const bt = boundRustType(n, typedNodes, plan);
1437
+ if ("map" in n) {
1438
+ const m = n.map;
1439
+ const batched = m.batched === true;
1440
+ const arrRef = ref;
1441
+ const elemRowRef = mapElemRowRef(n, arrRef, plan);
1442
+ const elemT = rawElemStructName(c.name, n.id);
1443
+ const batchT = rawRowStructName(c.name, n.id);
1444
+ const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
1445
+ const portsT = batched ? `${portsStructName(c.name, n.id)}Batch` : portsStructName(c.name, n.id);
1446
+ const retT = batched ? batchT : elemT;
1447
+ if (batched) {
1448
+ methods.push(` fn ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
1449
+ let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
1450
+ if is_err {
1451
+ return Some(${batchT} { is_error: true, err, ..Default::default() });
1452
+ }
1453
+ let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
1454
+ let rows = arr.iter().map(${elemDecode}).collect();
1455
+ Some(${batchT} { rows, ..Default::default() })
1456
+ }`);
1457
+ }
1458
+ else {
1459
+ methods.push(` fn ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${elemT}> {
1460
+ let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
1461
+ if is_err {
1462
+ return Some(${elemT} { is_error: true, err, ..Default::default() });
1463
+ }
1464
+ let v = ok.unwrap_or(Value::Null);
1465
+ Some(${elemDecode}(&v))
1466
+ }`);
1467
+ }
1468
+ void retT;
1469
+ continue;
1470
+ }
1471
+ const node = n;
1472
+ const rowT = rawRowStructName(c.name, node.id);
1473
+ const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
1474
+ methods.push(` fn ${nodeMethodName(node.id)}(&self, _ports: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
1475
+ let (is_err, err, ok) = self.next(${rustStrLit(node.component)})?;
1476
+ if is_err {
1477
+ return Some(${rowT} { is_error: true, err, ..Default::default() });
1478
+ }
1479
+ let v = ok.unwrap_or(Value::Null);
1480
+ Some(${decode}(&v))
1481
+ }`);
1482
+ }
1483
+ traitImpls.push(`impl ${handlerTraitName(c.name)} for ScriptedNativeRaw {\n${methods.join("\n")}\n}`);
1484
+ }
1485
+ const arms = native
1486
+ .map((c) => {
1487
+ const outRef = c.outputType !== undefined ? deriveTypeRef(c.outputType, plan) : undefined;
1488
+ const ser = outRef ? serializeTyped("tv", outRef, plan) : "tv";
1489
+ // bc#90 / runtime-free: the covered runner returns the module-LOCAL `BehaviorError` (via super::*);
1490
+ // bridge it to the bc-runtime `behavior_contracts::BehaviorError` at the observe boundary (same
1491
+ // code/message) so the conformance harness (main.rs) reads the stable code without the covered
1492
+ // module ever touching bc runtime. decode_* is test glue and returns the local error too.
1493
+ return ` ${rustStrLit(c.name)} => {\n let in_ = decode_${inStructName(c.name)}(input).map_err(to_rt_err)?;\n let tv = super::run_native_raw_struct_${sanitize(c.name)}(handlers, in_).map_err(to_rt_err)?;\n Ok(${ser})\n }`;
1494
+ })
1495
+ .join("\n");
1496
+ // the scripted handler is passed as `&ScriptedNativeRaw`; the observe entry takes `&ScriptedNativeRaw`
1497
+ // directly (the harness constructs it). Sync is inherent (the queues live behind a Mutex, seen behind
1498
+ // an atomic) so a concurrency stage runs it in parallel.
1499
+ const adapter = `// ScriptedNativeRaw — the CONCRETE test handler: it satisfies every HandlerNR<comp> trait by draining a
1500
+ // per-COMPONENT scripted-outcome queue and decoding the scripted ok Value into the concrete row struct.
1501
+ // The decode (decode_row_*) is generated (it knows the concrete row type), so the runner stays fully
1502
+ // concrete. Sync (queues behind a Mutex, the probe counter an atomic) so a bc#87 stage parallelizes it.
1503
+ pub struct ScriptedNativeRaw {
1504
+ queues: std::sync::Mutex<std::collections::HashMap<String, (Vec<serde_json::Value>, bool)>>,
1505
+ #[allow(dead_code)]
1506
+ seen: std::sync::atomic::AtomicUsize,
1507
+ }
1508
+
1509
+ impl ScriptedNativeRaw {
1510
+ // build from the JSON handlers spec: { "<component>": {ok|error} | [ {ok|error}, … ] }.
1511
+ pub fn new(spec: &serde_json::Map<String, serde_json::Value>) -> Self {
1512
+ let mut queues = std::collections::HashMap::new();
1513
+ for (name, raw) in spec {
1514
+ let (items, single) = match raw {
1515
+ serde_json::Value::Array(a) => (a.clone(), false),
1516
+ other => (vec![other.clone()], true),
1517
+ };
1518
+ queues.insert(name.clone(), (items, single));
1519
+ }
1520
+ ScriptedNativeRaw {
1521
+ queues: std::sync::Mutex::new(queues),
1522
+ seen: std::sync::atomic::AtomicUsize::new(0),
1523
+ }
1524
+ }
1525
+
1526
+ // drain one scripted outcome for a component (last entry repeats — mirrors the reference scripted
1527
+ // handlers); None when the component has no scripted queue (fail-closed). Returns (is_error, err,
1528
+ // ok_value). The bc#87 parallel-stage runner calls node_* from multiple threads, so the drain is
1529
+ // Mutex-guarded.
1530
+ fn next(&self, component: &str) -> Option<(bool, String, Option<Value>)> {
1531
+ self.seen.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1532
+ let mut queues = self.queues.lock().unwrap();
1533
+ let (queue, single) = queues.get_mut(component)?;
1534
+ let raw = if *single || queue.len() <= 1 { queue[0].clone() } else { queue.remove(0) };
1535
+ if let Some(okv) = raw.get("ok") {
1536
+ let v = behavior_contracts::decode_value(okv).expect("decode ok");
1537
+ Some((false, String::new(), Some(v)))
1538
+ } else {
1539
+ let e = raw.get("error").and_then(|e| e.as_str()).unwrap_or("").to_string();
1540
+ Some((true, e, None))
1541
+ }
1542
+ }
1543
+ }`;
1544
+ const observeBound = needSync ? "" : "";
1545
+ void observeBound;
1546
+ return `// GENERATED test-only observe companion (bc#77/#94) — NOT part of the covered module. Serializes the
1547
+ // struct-native runner's result to a canonical Value for the equivalence pin, and carries the CONCRETE
1548
+ // scripted test handler (ScriptedNativeRaw) that satisfies every HandlerNR<comp> trait by decoding a
1549
+ // scripted Value into the concrete per-node row. The consumer never uses this (it keeps the model native
1550
+ // + implements HandlerNR<comp> over its own wire payload); it exists only so the test harness can
1551
+ // byte-compare the STRUCT-only covered read against run_behavior. A submodule of the covered module
1552
+ // (reads its pub run_native_raw_struct_* + the CONCRETE structs/traits — incl. the LOCAL BehaviorError —
1553
+ // via super::*). bc#90 / runtime-free: the covered module's BehaviorError is LOCAL, so this test-glue
1554
+ // companion refers to it via super::* (as \`BehaviorError\`) and refers to the bc-runtime failure type
1555
+ // FULLY-QUALIFIED (\`behavior_contracts::BehaviorError\`) only where it bridges at the observe boundary.
1556
+ #![allow(dead_code, unused_imports)]
1557
+ use behavior_contracts::Value;
1558
+ use super::*;
1559
+
1560
+ // bridge the covered module's LOCAL BehaviorError (super::BehaviorError) to the bc-runtime failure type
1561
+ // the conformance harness reads (same code + message) — the ONLY place the two meet (test glue).
1562
+ fn to_rt_err(e: BehaviorError) -> behavior_contracts::BehaviorError {
1563
+ behavior_contracts::BehaviorError::new(e.code, e.message)
1564
+ }
1565
+
1566
+ ${serializers}
1567
+
1568
+ // Value -> typed struct marshallers for the concrete-row decode (TEST glue only).
1569
+ ${marshallers}
1570
+
1571
+ ${emitMarshalHelpers()}
1572
+
1573
+ // per-node concrete-row decoders (scripted Value -> RawRowNR_/RawElemNR_ struct).
1574
+ ${decodeFns.join("\n\n")}
1575
+
1576
+ // input decoders (generic &[(String,Value)] -> concrete InNR_<comp>; TEST glue, one decode at the boundary).
1577
+ ${inDecoders}
1578
+
1579
+ ${adapter}
1580
+
1581
+ // scripted handler trait impls (one per covered component — the concrete node_* seam, test glue).
1582
+ ${traitImpls.join("\n\n")}
1583
+
1584
+ // observe_native_raw drives the CONCRETE struct-native runner and serializes to a canonical Value (test
1585
+ // only). Takes the CONCRETE &ScriptedNativeRaw handler so each enclosed run_native_raw_struct_* call is
1586
+ // instantiated at the concrete handler type (direct per-node method calls), not at a trait object.
1587
+ pub fn observe_native_raw(
1588
+ name: &str,
1589
+ handlers: &ScriptedNativeRaw,
1590
+ input: &[(String, Value)],
1591
+ ) -> Result<Value, behavior_contracts::BehaviorError> {
1592
+ match name {
1593
+ ${arms}
1594
+ other => Err(behavior_contracts::BehaviorError::new("UNKNOWN_ENTRY", format!("component '{other}' is not a covered read (fail-closed)"))),
1595
+ }
1596
+ }
1597
+ `;
1598
+ }
1599
+ /** emitInDecoder — a TEST-glue decoder `decode_InNR_<comp>(input) -> Result<InNR_<comp>, BehaviorError>`.
1600
+ * Reads each declared input port from the generic &[(String,Value)] into the concrete field (scalar →
1601
+ * typed read; Value-typed → the raw Value clone). A missing key leaves Default (every covered input is
1602
+ * required + supplied by the corpus). Lives ONLY in the observe companion. */
1603
+ function emitInDecoder(comp, plan) {
1604
+ void plan;
1605
+ const name = inStructName(comp.name);
1606
+ const ports = Object.keys(comp.inputPorts ?? {});
1607
+ const lines = [];
1608
+ lines.push(`fn decode_${name}(input: &[(String, Value)]) -> Result<${name}, BehaviorError> {`);
1609
+ lines.push(` let mut in_ = ${name}::default();`);
1610
+ if (ports.length === 0 || comp.inputPorts === undefined) {
1611
+ lines.push(` let _ = input;`);
1612
+ lines.push(` Ok(in_)`);
1613
+ lines.push(`}`);
1614
+ return lines.join("\n");
1615
+ }
1616
+ // an if/else-if chain (clippy-clean regardless of port count — a single-arm `match` trips single_match).
1617
+ lines.push(` for (k, v) in input {`);
1618
+ ports.forEach((p, i) => {
1619
+ const field = rustFieldName(p);
1620
+ const scalar = inputScalarKind(comp.inputPorts[p]);
1621
+ const head = i === 0 ? " if" : " } else if";
1622
+ lines.push(`${head} k == ${rustStrLit(p)} {`);
1623
+ if (scalar === undefined) {
1624
+ lines.push(` in_.${field} = v.clone();`);
1625
+ }
1626
+ else {
1627
+ const pat = scalar === "String" ? "Value::Str(sv)" : scalar === "i64" ? "Value::Int(sv)" : scalar === "f64" ? "Value::Float(sv)" : "Value::Bool(sv)";
1628
+ const label = scalar === "String" ? "string" : scalar === "i64" ? "int" : scalar === "f64" ? "float" : "bool";
1629
+ const bind = scalar === "String" ? "sv.clone()" : "*sv";
1630
+ // bc#90 / runtime-free: build the module-LOCAL BehaviorError (TYPE_MISMATCH) via the local
1631
+ // marshal_type_error helper — decode_* returns the local error, bridged at the observe boundary.
1632
+ lines.push(` match v { ${pat} => in_.${field} = ${bind}, other => return Err(marshal_type_error(${rustStrLit(`decode input ${p}: expected ${label}, got `)}, other.type_name())) }`);
1633
+ }
1634
+ });
1635
+ lines.push(` }`);
1636
+ lines.push(` }`);
1637
+ lines.push(` Ok(in_)`);
1638
+ lines.push(`}`);
1639
+ return lines.join("\n");
1640
+ }
1641
+ /** emitDecodeRow — emit (once, deduped) a decoder `decode_row_<comp>_<node><suffix>(v: &Value) -> <rowT>`
1642
+ * that materializes the scripted ok Value into the concrete row struct. Named: reuse the Value->struct
1643
+ * marshaller + move fields; scalar/arr/opt: materialize into .val. TEST glue only. */
1644
+ function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix) {
1645
+ const fn = `decode_row_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}${suffix.toLowerCase()}`;
1646
+ if (emitted.has(fn))
1647
+ return fn;
1648
+ emitted.add(fn);
1649
+ const lines = [];
1650
+ lines.push(`fn ${fn}(v: &Value) -> ${rowT} {`);
1651
+ if (ref.kind === "named") {
1652
+ const decl = findDecl(plan, ref.name);
1653
+ lines.push(` let t = marshal_${ref.name}(v).expect("scripted row decode");`);
1654
+ const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: t.${rustFieldName(f.name)}`).join(", ");
1655
+ lines.push(` ${rowT} { ${inits}, ..Default::default() }`);
1656
+ }
1657
+ else {
1658
+ const mat = materializeExpr("v", ref, plan);
1659
+ lines.push(` let val = (|| -> Result<${renderTypeRef(ref)}, BehaviorError> { Ok(${mat}) })().expect("scripted row decode");`);
1660
+ lines.push(` ${rowT} { val, ..Default::default() }`);
1661
+ }
1662
+ lines.push(`}`);
1663
+ out.push(lines.join("\n"));
1664
+ return fn;
1665
+ }
1666
+ export const rustTypedNativeEmitter = {
1667
+ language: "rust-typed-native",
1668
+ fileExtension: ".rs",
1669
+ defaultRuntimeImport: "behavior_contracts",
1670
+ filenameHint: "behaviors_typed_native_generated.rs",
1671
+ emit,
1672
+ };
1673
+ //# sourceMappingURL=emit-typed-native-rust.js.map