behavior-contracts 0.8.6 → 0.8.8

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 (53) hide show
  1. package/cli-contract.yaml +189 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +76 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/codegen.d.ts +6 -0
  7. package/dist/commands/codegen.d.ts.map +1 -0
  8. package/dist/commands/codegen.js +39 -0
  9. package/dist/commands/codegen.js.map +1 -0
  10. package/dist/generated/commands.d.ts +8 -0
  11. package/dist/generated/commands.d.ts.map +1 -0
  12. package/dist/generated/commands.js +55 -0
  13. package/dist/generated/commands.js.map +1 -0
  14. package/dist/generated/contract.d.ts +3 -0
  15. package/dist/generated/contract.d.ts.map +1 -0
  16. package/dist/generated/contract.js +5 -0
  17. package/dist/generated/contract.js.map +1 -0
  18. package/dist/generated/index.d.ts +7 -0
  19. package/dist/generated/index.d.ts.map +1 -0
  20. package/dist/generated/index.js +7 -0
  21. package/dist/generated/index.js.map +1 -0
  22. package/dist/generated/program.d.ts +17 -0
  23. package/dist/generated/program.d.ts.map +1 -0
  24. package/dist/generated/program.js +117 -0
  25. package/dist/generated/program.js.map +1 -0
  26. package/dist/generated/schemas.d.ts +2 -0
  27. package/dist/generated/schemas.d.ts.map +1 -0
  28. package/dist/generated/schemas.js +3 -0
  29. package/dist/generated/schemas.js.map +1 -0
  30. package/dist/generated/types.d.ts +35 -0
  31. package/dist/generated/types.d.ts.map +1 -0
  32. package/dist/generated/types.js +3 -0
  33. package/dist/generated/types.js.map +1 -0
  34. package/dist/generator/core.d.ts +28 -0
  35. package/dist/generator/core.d.ts.map +1 -1
  36. package/dist/generator/core.js +7 -0
  37. package/dist/generator/core.js.map +1 -1
  38. package/dist/generator/emit-shared-debox.d.ts +108 -0
  39. package/dist/generator/emit-shared-debox.d.ts.map +1 -0
  40. package/dist/generator/emit-shared-debox.js +117 -0
  41. package/dist/generator/emit-shared-debox.js.map +1 -0
  42. package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
  43. package/dist/generator/emit-typed-native-go.js +436 -1078
  44. package/dist/generator/emit-typed-native-go.js.map +1 -1
  45. package/dist/generator/emit-typed-native-rust.d.ts +9 -7
  46. package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
  47. package/dist/generator/emit-typed-native-rust.js +313 -844
  48. package/dist/generator/emit-typed-native-rust.js.map +1 -1
  49. package/dist/generator/typed.d.ts +4 -2
  50. package/dist/generator/typed.d.ts.map +1 -1
  51. package/dist/generator/typed.js +4 -2
  52. package/dist/generator/typed.js.map +1 -1
  53. package/package.json +12 -2
@@ -6,6 +6,7 @@ import { buildTypePlan, deriveTypeRef, findDecl, inputPortIsOptional, inputPortT
6
6
  import { concurrencyPlan, execPlan, analyzeSequentialOps, classifyExpr } from "./straightline.js";
7
7
  import { NativeExprCompiler } from "./native-expr.js";
8
8
  import { buildAsyncPlans } from "./async-plan.js";
9
+ import { buildDeBoxPlan, notationOfRef } from "./emit-shared-debox.js";
9
10
  const { sanitize, rustStrLit } = rustStraightlineInternals;
10
11
  const { typeRefIsCopy, rustFieldName, renderTypeRef, typedCell, typedFieldAccess, serializeTyped, emitTypeDecls, emitDefaults, emitSerializers, emitTypedValue, emitMarshallers, emitMarshalHelpers, materializeExpr, } = rustTypedInternals;
11
12
  /** rustErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
@@ -172,14 +173,6 @@ function pascal(s) {
172
173
  function portsStructName(compName, nodeId) {
173
174
  return `PortsNR${pascal(compName)}${pascal(nodeId)}`;
174
175
  }
175
- /** RawRowNR<Comp><Node> — the concrete per-node handler-result row struct name. */
176
- function rawRowStructName(compName, nodeId) {
177
- return `RawRowNR${pascal(compName)}${pascal(nodeId)}`;
178
- }
179
- /** RawElemNR<Comp><Node> — the concrete per-ELEMENT row struct name for a covered map node. */
180
- function rawElemStructName(compName, nodeId) {
181
- return `RawElemNR${pascal(compName)}${pascal(nodeId)}`;
182
- }
183
176
  /** HandlerNR<Comp> — the per-component concrete handler trait name (one node_* method per node). */
184
177
  function handlerTraitName(compName) {
185
178
  return `HandlerNR${pascal(compName)}`;
@@ -192,56 +185,17 @@ function nodeMethodName(nodeId) {
192
185
  function inStructName(compName) {
193
186
  return `InNR${pascal(compName)}`;
194
187
  }
195
- function recordTop(ref) {
196
- if (ref.kind === "named")
197
- return { kind: "named", innerName: ref.name };
198
- if (ref.kind === "arr" && ref.elem.kind === "named")
199
- return { kind: "arr", innerName: ref.elem.name };
200
- if (ref.kind === "opt" && ref.inner.kind === "named")
201
- return { kind: "opt", innerName: ref.inner.name };
202
- if (ref.kind === "map" && ref.value.kind === "named")
203
- return { kind: "map", innerName: ref.value.name };
204
- return null;
205
- }
206
- /** deBoxEligible — a componentRef node whose result is a record-containing top (named / arr(named) /
207
- * opt(named) / map(named)). Its handler returns the wire; the generated decode probes each record. */
208
- function deBoxEligible(ref) {
209
- return recordTop(ref) !== null;
210
- }
211
- /** rustWireTypeForTop — the Rust handler return type (over the associated Self::Wire) for a record top. */
212
- function rustWireTypeForTop(top) {
213
- switch (top.kind) {
214
- case "named":
215
- return "Self::Wire";
216
- case "arr":
217
- return "Vec<Self::Wire>";
218
- case "opt":
219
- return "Option<Self::Wire>";
220
- case "map":
221
- return "std::collections::BTreeMap<String, Self::Wire>";
222
- }
223
- }
224
- /** notationOfRef — the Portable Type Notation of a TypeRef (scp-error.md), resolving a named ref to its
225
- * obj notation via the plan. Baked as the `expectedType` literal (a rendering of the STATICALLY declared
226
- * type — nothing walks a type at runtime). */
227
- function notationOfRef(ref, plan) {
228
- switch (ref.kind) {
229
- case "scalar":
230
- return ref.scalar;
231
- case "opt":
232
- return `opt(${notationOfRef(ref.inner, plan)})`;
233
- case "arr":
234
- return `arr(${notationOfRef(ref.elem, plan)})`;
235
- case "map":
236
- return `map(${notationOfRef(ref.value, plan)})`;
237
- case "named": {
238
- const decl = findDecl(plan, ref.name);
239
- return `obj{${decl.fields.map((f) => `${f.name}:${notationOfRef(f.type, plan)}`).join(",")}}`;
240
- }
241
- }
242
- }
243
- /** emitRustProbeWireTypes — the once-per-module probe enums + WireRow/WireList seam traits. Concrete
244
- * enums; monomorphized traits (mutual Row/List associated types keep nesting concrete, no dyn/Box). */
188
+ // ── strict de-box: the probe/wire seam + generated decode orchestration ───────────────────────
189
+ //
190
+ // A covered record read returns the consumer's WIRE (a value implementing WireRow), and the generated
191
+ // decode probes it per field from the declared type. The consumer supplies variant classification (a
192
+ // probe reports the producer's own wire tag as a free string); the generated code owns strictness —
193
+ // required/optional, present/absent, error assembly, fail-closed. The seam stays native: Probe/NumProbe
194
+ // are concrete enums and WireRow/WireList are monomorphized via mutual associated types (Row/List), so no
195
+ // boxed Value / dyn / Box crosses the boundary.
196
+ /** emitRustProbeWireTypes the once-per-module probe enums + WireValue/WireRow/WireList seam traits.
197
+ * Concrete enums; monomorphized traits (WireValue's Row associated type threads WireRow, whose List
198
+ * threads WireList — nesting stays concrete, no dyn/Box). */
245
199
  function emitRustProbeWireTypes() {
246
200
  return `// Probe<T> / NumProbe — the outcome of classifying one wire attribute against a declared type. Got carries
247
201
  // the matched value (a NumProbe's raw numeric text, which the de-box parses + range-checks so overflow is
@@ -261,6 +215,20 @@ pub enum NumProbe {
261
215
  Absent,
262
216
  }
263
217
 
218
+ // WireValue — a node result's own wire datum (the uniform handler return): the producer's value at a
219
+ // result-consumption point. The generated de-box classifies it against the STATICALLY declared node type
220
+ // via these inherent methods (as_row / as_list yield the WireRow / WireList the nested decode then probes).
221
+ // A top value is always present, so there is no Absent — a producer-null is Null. Monomorphized (Row
222
+ // associated type threads WireRow) — no dyn, no Box, no boxed value crosses the seam.
223
+ pub trait WireValue: Sized {
224
+ type Row: WireRow;
225
+ fn as_string(&self) -> Probe<String>;
226
+ fn as_number(&self) -> NumProbe;
227
+ fn as_bool(&self) -> Probe<bool>;
228
+ fn as_row(&self) -> Probe<Self::Row>;
229
+ fn as_list(&self) -> Probe<<Self::Row as WireRow>::List>;
230
+ }
231
+
264
232
  // WireRow / WireList — the consumer's wire item / wire array, probed per declared field / element by the
265
233
  // generated de-box. The consumer implements them over its wire payload (a DynamoDB AttributeValue map)
266
234
  // and classifies each attribute's variant; the de-box owns strictness. Monomorphized (Row/List mutual
@@ -289,147 +257,124 @@ pub trait WireList: Sized {
289
257
  fn elem_list(&self, i: usize) -> Probe<Self>;
290
258
  }`;
291
259
  }
292
- /** rustDecodeNamedName — the decode function name for a named type (deduped across the module). */
293
- function rustDecodeNamedName(name) {
294
- return `decode_wire_${name.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase()}`;
295
- }
296
- /**
297
- * emitRustDecodeNamed — emit (once, deduped) `decode_wire_<name><W: WireRow>(w: &W) -> Result<Name,
298
- * BehaviorError>`: probe each declared field from the type SSoT (via the single rustDecodeValue
299
- * recursion) and build the struct, failing closed with the structured Error Value on a mismatch.
300
- */
301
- function emitRustDecodeNamed(name, plan, emitted, out) {
302
- const fn = rustDecodeNamedName(name);
303
- if (emitted.has(fn))
304
- return fn;
305
- emitted.add(fn);
306
- const decl = findDecl(plan, name);
307
- const lines = [];
308
- lines.push(`// ${fn} — the strict de-box for '${name}': probe each declared field, fail closed on a`);
309
- lines.push(`// wire mismatch with the structured Error Value (byte-equal codes to run_behavior).`);
310
- lines.push(`fn ${fn}<W: WireRow>(w: &W) -> Result<${name}, BehaviorError> {`);
311
- const ctr = { n: 0 };
312
- const binds = [];
313
- for (const f of decl.fields) {
314
- const rf = rustFieldName(f.name);
315
- const expr = rustDecodeValue(f.type, "w", rustStrLit(f.name), "row", name, f.name, plan, emitted, out, ctr, 1);
316
- lines.push(` let ${rf} = ${expr};`);
317
- binds.push(rf);
318
- }
319
- lines.push(` Ok(${name} { ${binds.join(", ")} })`);
320
- lines.push(`}`);
321
- out.push(lines.join("\n"));
322
- return fn;
323
- }
324
260
  /**
325
- * rustDecodeValue — the SINGLE recursive decode over the full TypeRef grammar (scalar / opt(scalar) /
326
- * named / arr / map). Returns a Rust expression (a match block) that evaluates to the decoded value of
327
- * `ref`, obtained by probing `srcExpr` at `keyExpr` (a WireRow field via `probe_*`, srcKind "row"; or a
328
- * WireList element via `elem_*`, srcKind "list"). Every nested position (arr element, map value, obj
329
- * field) recurses through THIS function, so arbitrary nesting works by construction. A mismatch does
330
- * `return Err(<structured error>)`; no panic, no boxed value.
261
+ * renderRustDeBox — the ONE inline, recursive strict de-box renderer. Folds a language-neutral
262
+ * {@link DeBoxPlan} (the SSoT walk) into a single Rust EXPRESSION that evaluates to the decoded native
263
+ * value of the plan's type, obtained by probing `srcExpr` per the node's access (top `as_*`, field
264
+ * `probe_*(key)`, elem `elem_*(i)`, mapval → `probe_*(&k)`). The recursion runs HERE at generation
265
+ * time (walking the plan), so the emitted Rust is FLAT, CONCRETE, fully-unrolled code: NO `decode_wire_*`
266
+ * helper function, NO output-side recursion a nested record expands to a nested struct literal, a
267
+ * nested collection to a nested loop. A required-position mismatch does `return Err(de_*(...))` (fail
268
+ * closed, propagating the structured Error Value from the enclosing runner); an optional position
269
+ * (optDepth>0) maps absent/null to the all-None value. `ctr` hands out unique temp names (`sub2`, `l3`,
270
+ * `m4`, `i3`, `k4`) so nested blocks never collide. `idxExpr` is the element index / map-key argument the
271
+ * enclosing list / map loop supplies (empty for top / field). Mirrors the interpreter's outType check +
272
+ * the (now-deleted) per-position decoders, but TOTAL over the grammar (opt(named)/arr(arr)/map(opt)…) and
273
+ * inlined — the type is baked all the way through by the TS compiler, not the target monomorphizer.
331
274
  */
332
- function rustDecodeValue(ref, srcExpr, keyExpr, srcKind, model, fieldRaw, plan, emitted, out, ctr, indent) {
333
- const I = " ".repeat(indent);
334
- const I1 = " ".repeat(indent + 1);
335
- const ml = rustStrLit(model);
336
- const fl = rustStrLit(fieldRaw);
337
- const nl = rustStrLit(notationOfRef(ref, plan));
275
+ function renderRustDeBox(plan, srcExpr, ctr, indent, fail, idxExpr = "") {
276
+ const I = indent;
277
+ const I1 = indent + " ";
338
278
  const id = ctr.n++;
339
- const prefix = srcKind === "row" ? "probe_" : "elem_";
340
- const mismatch = `return Err(de_type_mismatch(${ml}, ${fl}, ${nl}, actual_wire_type, raw_value))`;
341
- const missing = `return Err(de_missing_field(${ml}, ${fl}, ${nl}))`;
342
- if (ref.kind === "opt" && ref.inner.kind === "scalar" && ref.inner.scalar !== "null") {
343
- const inner = ref.inner.scalar;
344
- if (inner === "int" || inner === "float") {
345
- const rustT = inner === "int" ? "i64" : "f64";
346
- return `match ${srcExpr}.${prefix}number(${keyExpr}) {
347
- ${I1}NumProbe::Got { raw, actual_wire_type } => match raw.parse::<${rustT}>() {
348
- ${I1} Ok(n) => Some(n),
349
- ${I1} Err(_) => return Err(de_overflow(${ml}, ${fl}, ${nl}, actual_wire_type, raw)),
350
- ${I1}},
351
- ${I1}NumProbe::Wrong { actual_wire_type, raw_value } => ${mismatch},
352
- ${I1}NumProbe::Null { .. } | NumProbe::Absent => None,
353
- ${I}}`;
279
+ const ml = rustStrLit(plan.model);
280
+ const fl = rustStrLit(plan.field);
281
+ const nl = rustStrLit(plan.expectedType);
282
+ const opt = plan.optDepth > 0;
283
+ const mismatch = fail(`de_type_mismatch(${ml}, ${fl}, ${nl}, actual_wire_type, raw_value)`);
284
+ const missing = fail(`de_missing_field(${ml}, ${fl}, ${nl})`);
285
+ // the probe expression for this node's access + a value-kind suffix (string/number/bool/row/list).
286
+ const probe = (suffix) => {
287
+ switch (plan.access.from) {
288
+ case "top":
289
+ return `${srcExpr}.as_${suffix}()`;
290
+ case "field":
291
+ return `${srcExpr}.probe_${suffix}(${rustStrLit(plan.access.key)})`;
292
+ case "elem":
293
+ return `${srcExpr}.elem_${suffix}(${idxExpr})`;
294
+ case "mapval":
295
+ return `${srcExpr}.probe_${suffix}(${idxExpr})`;
354
296
  }
355
- const meth = inner === "string" ? "string" : "bool";
356
- return `match ${srcExpr}.${prefix}${meth}(${keyExpr}) {
357
- ${I1}Probe::Got(v) => Some(v),
358
- ${I1}Probe::Wrong { actual_wire_type, raw_value } => ${mismatch},
359
- ${I1}Probe::Null { .. } | Probe::Absent => None,
360
- ${I}}`;
361
- }
362
- if (ref.kind === "scalar" && ref.scalar !== "null") {
363
- if (ref.scalar === "int" || ref.scalar === "float") {
364
- const rustT = ref.scalar === "int" ? "i64" : "f64";
365
- return `match ${srcExpr}.${prefix}number(${keyExpr}) {
297
+ };
298
+ // wrap the decoded value in optDepth `Some(...)` layers (present ⇒ Some^N(v); absent/null ⇒ None).
299
+ const wrapSome = (expr) => {
300
+ let e = expr;
301
+ for (let i = 0; i < plan.optDepth; i++)
302
+ e = `Some(${e})`;
303
+ return e;
304
+ };
305
+ // the three non-Got Probe<T> arms (scalar string/bool, record, list, map): required ⇒ wrong/null
306
+ // mismatch + absent missing; optional wrong mismatch, null/absent None.
307
+ const probeArms = opt
308
+ ? `${I1}Probe::Wrong { actual_wire_type, raw_value } => ${mismatch},\n${I1}Probe::Null { .. } | Probe::Absent => None,`
309
+ : `${I1}Probe::Wrong { actual_wire_type, raw_value }\n${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},\n${I1}Probe::Absent => ${missing},`;
310
+ if (plan.k === "scalar") {
311
+ if (plan.scalar === "int" || plan.scalar === "float") {
312
+ const rustT = plan.scalar === "int" ? "i64" : "f64";
313
+ const numArms = opt
314
+ ? `${I1}NumProbe::Wrong { actual_wire_type, raw_value } => ${mismatch},\n${I1}NumProbe::Null { .. } | NumProbe::Absent => None,`
315
+ : `${I1}NumProbe::Wrong { actual_wire_type, raw_value }\n${I1}| NumProbe::Null { actual_wire_type, raw_value } => ${mismatch},\n${I1}NumProbe::Absent => ${missing},`;
316
+ return `match ${probe("number")} {
366
317
  ${I1}NumProbe::Got { raw, actual_wire_type } => match raw.parse::<${rustT}>() {
367
- ${I1} Ok(n) => n,
368
- ${I1} Err(_) => return Err(de_overflow(${ml}, ${fl}, ${nl}, actual_wire_type, raw)),
318
+ ${I1} Ok(n) => ${wrapSome("n")},
319
+ ${I1} Err(_) => ${fail(`de_overflow(${ml}, ${fl}, ${nl}, actual_wire_type, raw)`)},
369
320
  ${I1}},
370
- ${I1}NumProbe::Wrong { actual_wire_type, raw_value }
371
- ${I1}| NumProbe::Null { actual_wire_type, raw_value } => ${mismatch},
372
- ${I1}NumProbe::Absent => ${missing},
321
+ ${numArms}
373
322
  ${I}}`;
374
323
  }
375
- const meth = ref.scalar === "string" ? "string" : "bool";
376
- return `match ${srcExpr}.${prefix}${meth}(${keyExpr}) {
377
- ${I1}Probe::Got(v) => v,
378
- ${I1}Probe::Wrong { actual_wire_type, raw_value }
379
- ${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
380
- ${I1}Probe::Absent => ${missing},
324
+ const meth = plan.scalar === "string" ? "string" : "bool";
325
+ return `match ${probe(meth)} {
326
+ ${I1}Probe::Got(v) => ${wrapSome("v")},
327
+ ${probeArms}
381
328
  ${I}}`;
382
329
  }
383
- if (ref.kind === "named") {
384
- const dec = emitRustDecodeNamed(ref.name, plan, emitted, out);
385
- const rowMeth = srcKind === "row" ? "probe_row" : "elem_row";
386
- return `match ${srcExpr}.${rowMeth}(${keyExpr}) {
387
- ${I1}Probe::Got(sub) => ${dec}(&sub)?,
388
- ${I1}Probe::Wrong { actual_wire_type, raw_value }
389
- ${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
390
- ${I1}Probe::Absent => ${missing},
330
+ if (plan.k === "record") {
331
+ const fieldInits = plan.fields
332
+ .map((f) => `${I1} ${rustFieldName(f.name)}: ${renderRustDeBox(f.plan, `sub${id}`, ctr, `${I1} `, fail)},`)
333
+ .join("\n");
334
+ return `match ${probe("row")} {
335
+ ${I1}Probe::Got(sub${id}) => ${wrapSome(`${plan.typeName} {
336
+ ${fieldInits}
337
+ ${I1}}`)},
338
+ ${probeArms}
391
339
  ${I}}`;
392
340
  }
393
- if (ref.kind === "arr") {
394
- const listMeth = srcKind === "row" ? "probe_list" : "elem_list";
395
- const elemExpr = rustDecodeValue(ref.elem, `l${id}`, `i${id}`, "list", model, fieldRaw, plan, emitted, out, ctr, indent + 2);
396
- return `match ${srcExpr}.${listMeth}(${keyExpr}) {
397
- ${I1}Probe::Got(l${id}) => {
398
- ${I1} let mut acc = Vec::with_capacity(l${id}.len());
341
+ if (plan.k === "list") {
342
+ const elemExpr = renderRustDeBox(plan.elem, `l${id}`, ctr, `${I1} `, fail, `i${id}`);
343
+ return `match ${probe("list")} {
344
+ ${I1}Probe::Got(l${id}) => ${wrapSome(`{
345
+ ${I1} let mut acc${id} = Vec::with_capacity(l${id}.len());
399
346
  ${I1} for i${id} in 0..l${id}.len() {
400
- ${I1} acc.push(${elemExpr});
347
+ ${I1} acc${id}.push(${elemExpr});
401
348
  ${I1} }
402
- ${I1} acc
403
- ${I1}}
404
- ${I1}Probe::Wrong { actual_wire_type, raw_value }
405
- ${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
406
- ${I1}Probe::Absent => ${missing},
349
+ ${I1} acc${id}
350
+ ${I1}}`)},
351
+ ${probeArms}
407
352
  ${I}}`;
408
353
  }
409
- if (ref.kind === "map") {
410
- // a declared map(V): the wire value is a producer map (a DynamoDB "M"); enumerate its keys and decode
411
- // each value as V through THIS same recursion (so map-of-map / map-of-arr nest by construction).
412
- const rowMeth = srcKind === "row" ? "probe_row" : "elem_row";
413
- const valExpr = rustDecodeValue(ref.value, `m${id}`, `&k${id}`, "row", model, fieldRaw, plan, emitted, out, ctr, indent + 2);
414
- return `match ${srcExpr}.${rowMeth}(${keyExpr}) {
415
- ${I1}Probe::Got(m${id}) => {
416
- ${I1} let mut acc = std::collections::BTreeMap::new();
354
+ // map(V): the wire value is a producer map (a DynamoDB "M"); enumerate its keys, decode each value as V.
355
+ const valExpr = renderRustDeBox(plan.value, `m${id}`, ctr, `${I1} `, fail, `&k${id}`);
356
+ return `match ${probe("row")} {
357
+ ${I1}Probe::Got(m${id}) => ${wrapSome(`{
358
+ ${I1} let mut acc${id} = std::collections::BTreeMap::new();
417
359
  ${I1} for k${id} in m${id}.keys() {
418
360
  ${I1} let v${id} = ${valExpr};
419
- ${I1} acc.insert(k${id}, v${id});
361
+ ${I1} acc${id}.insert(k${id}, v${id});
420
362
  ${I1} }
421
- ${I1} acc
422
- ${I1}}
423
- ${I1}Probe::Wrong { actual_wire_type, raw_value }
424
- ${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
425
- ${I1}Probe::Absent => ${missing},
363
+ ${I1} acc${id}
364
+ ${I1}}`)},
365
+ ${probeArms}
426
366
  ${I}}`;
427
- }
428
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: '${fieldRaw}' of '${model}' has type ${JSON.stringify(ref)} which the generated de-box does not decode (supported: scalar / opt(scalar) / named / arr / map).`);
429
367
  }
430
- /** rustDecodeElemName — the element de-box function name for a map/fanout node. */
431
- function rustDecodeElemName(compName, nodeId) {
432
- return `decode_wire_elem_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase()}`;
368
+ /** returnErrFail — the default de-box mismatch control flow: propagate the structured Error Value out of
369
+ * the enclosing runner (fail/retry componentRef results + map/fanout element de-box, which fails closed
370
+ * regardless of elementPolicy — matching the interpreter's whole-node outType throw). */
371
+ const returnErrFail = (errValueExpr) => `return Err(${errValueExpr})`;
372
+ /** renderResultDeBox — inline strict de-box of a whole result WIRE (`srcExpr`, a WireValue) into its
373
+ * native value, from the TOTAL SSoT plan for `ref` at the top position. `fail` selects the mismatch
374
+ * control flow. This is the ONE de-box call the runner arms share (componentRef result + map/fanout
375
+ * element) — no per-position predicate, no helper decode fn. */
376
+ function renderResultDeBox(ref, srcExpr, nodeId, plan, indent, fail = returnErrFail) {
377
+ return renderRustDeBox(buildDeBoxPlan(ref, { from: "top" }, { model: nodeId, field: nodeId, plan }), srcExpr, { n: 0 }, indent, fail);
433
378
  }
434
379
  /** mapFanoutElemRef — the element ROW TypeRef a map/fanout node decodes (the per-element record). */
435
380
  function mapFanoutElemRef(node, typedNodes, plan) {
@@ -438,185 +383,6 @@ function mapFanoutElemRef(node, typedNodes, plan) {
438
383
  return fanoutItemsElemRef(node, ref, plan);
439
384
  return mapElemRowRef(node, ref, plan);
440
385
  }
441
- /** mapFanoutElemDeBoxEligible — a map/fanout node whose element ROW is a named record de-boxes each
442
- * element through the generated decode_wire_elem_* (closing the scan-row silent-default hole). */
443
- function mapFanoutElemDeBoxEligible(node, typedNodes, plan) {
444
- return mapFanoutElemRef(node, typedNodes, plan).kind === "named";
445
- }
446
- /**
447
- * emitRustDecodeElemRow — emit (once) `decode_wire_elem_<comp>_<node><W: WireRow>(w: &W) ->
448
- * Result<RawElemNR<comp><node>, BehaviorError>` for a map/fanout node whose element ROW is a named
449
- * record: probe each declared field via the SAME rustDecodeValue recursion as record reads and build the
450
- * concrete RawElemNR struct, failing closed with the structured Error Value on a mismatch.
451
- */
452
- function emitRustDecodeElemRow(comp, node, elemRowRef, plan, emitted, out) {
453
- const nodeId = node.id;
454
- const fn = rustDecodeElemName(comp.name, nodeId);
455
- if (emitted.has(fn))
456
- return fn;
457
- emitted.add(fn);
458
- const retType = rawElemStructName(comp.name, nodeId);
459
- const decl = findDecl(plan, elemRowRef.name);
460
- const lines = [];
461
- lines.push(`// ${fn} — the strict de-box for a '${nodeId}' element row: probe each declared field, fail`);
462
- lines.push(`// closed on a wire mismatch with the structured Error Value (byte-equal codes to run_behavior).`);
463
- lines.push(`fn ${fn}<W: WireRow>(w: &W) -> Result<${retType}, BehaviorError> {`);
464
- const ctr = { n: 0 };
465
- const binds = [];
466
- for (const f of decl.fields) {
467
- const rf = rustFieldName(f.name);
468
- const expr = rustDecodeValue(f.type, "w", rustStrLit(f.name), "row", elemRowRef.name, f.name, plan, emitted, out, ctr, 1);
469
- lines.push(` let ${rf} = ${expr};`);
470
- binds.push(rf);
471
- }
472
- lines.push(` Ok(${retType} { ${binds.join(", ")} })`);
473
- lines.push(`}`);
474
- out.push(lines.join("\n"));
475
- return fn;
476
- }
477
- /** emitRustNodeDecodeFns — emit the decode functions for every de-box-eligible node: a componentRef
478
- * record read (decode_wire_<name>) OR a map/fanout element row (decode_wire_elem_<comp>_<node>). */
479
- /** rustDecodeResultName — the top-result decode function name for a de-box componentRef node. */
480
- function rustDecodeResultName(compName, nodeId) {
481
- return `decode_wire_result_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase()}`;
482
- }
483
- /**
484
- * emitRustDecodeWireResult — emit (once) `decode_wire_result_<comp>_<node><W: WireRow>(wire: <WireArg>)
485
- * -> Result<<ResultType>, BehaviorError>` for a componentRef node whose result is a record-containing
486
- * top: it decodes each record via decode_wire_<name> and assembles the top (record / Vec / Option /
487
- * BTreeMap). One path covers named / arr(named) / opt(named) / map(named).
488
- */
489
- function emitRustDecodeWireResult(comp, node, ref, plan, emitted, out) {
490
- const top = recordTop(ref);
491
- const fn = rustDecodeResultName(comp.name, node.id);
492
- if (emitted.has(fn))
493
- return fn;
494
- emitted.add(fn);
495
- const innerDecode = emitRustDecodeNamed(top.innerName, plan, emitted, out);
496
- const resultType = renderTypeRef(ref);
497
- const lines = [];
498
- lines.push(`// ${fn} — decode the node's record-containing '${top.kind}' result from the wire (each record`);
499
- lines.push(`// via ${innerDecode}); a de-box mismatch fails closed with the structured Error Value.`);
500
- switch (top.kind) {
501
- case "named":
502
- lines.push(`fn ${fn}<W: WireRow>(wire: W) -> Result<${resultType}, BehaviorError> {`);
503
- lines.push(` ${innerDecode}(&wire)`);
504
- break;
505
- case "arr":
506
- lines.push(`fn ${fn}<W: WireRow>(wire: Vec<W>) -> Result<${resultType}, BehaviorError> {`);
507
- lines.push(` let mut out = Vec::with_capacity(wire.len());`);
508
- lines.push(` for w in wire {`);
509
- lines.push(` out.push(${innerDecode}(&w)?);`);
510
- lines.push(` }`);
511
- lines.push(` Ok(out)`);
512
- break;
513
- case "opt":
514
- lines.push(`fn ${fn}<W: WireRow>(wire: Option<W>) -> Result<${resultType}, BehaviorError> {`);
515
- lines.push(` match wire {`);
516
- lines.push(` Some(w) => Ok(Some(${innerDecode}(&w)?)),`);
517
- lines.push(` None => Ok(None),`);
518
- lines.push(` }`);
519
- break;
520
- case "map":
521
- lines.push(`fn ${fn}<W: WireRow>(wire: std::collections::BTreeMap<String, W>) -> Result<${resultType}, BehaviorError> {`);
522
- lines.push(` let mut out = std::collections::BTreeMap::new();`);
523
- lines.push(` for (k, w) in wire {`);
524
- lines.push(` out.insert(k, ${innerDecode}(&w)?);`);
525
- lines.push(` }`);
526
- lines.push(` Ok(out)`);
527
- break;
528
- }
529
- lines.push(`}`);
530
- out.push(lines.join("\n"));
531
- return fn;
532
- }
533
- function emitRustNodeDecodeFns(comp, typedNodes, plan, emitted, out) {
534
- for (const n of comp.body) {
535
- if ("cond" in n)
536
- continue;
537
- if ("map" in n || "fanout" in n) {
538
- const node = n;
539
- if (!mapFanoutElemDeBoxEligible(node, typedNodes, plan))
540
- continue;
541
- emitRustDecodeElemRow(comp, node, mapFanoutElemRef(node, typedNodes, plan), plan, emitted, out);
542
- continue;
543
- }
544
- const ref = typedNodes.get(n.id);
545
- if (ref === undefined)
546
- continue;
547
- const top = recordTop(ref);
548
- if (top === null)
549
- continue;
550
- // a NAMED top decodes in place via decode_wire_<name>; an arr/opt/map top assembles via
551
- // decode_wire_result_* (which recurses into decode_wire_<name> per record).
552
- if (top.kind === "named")
553
- emitRustDecodeNamed(top.innerName, plan, emitted, out);
554
- else
555
- emitRustDecodeWireResult(comp, n, ref, plan, emitted, out);
556
- }
557
- }
558
- /** containsNamed — whether a TypeRef transitively contains a named record (through arr / opt / map). */
559
- function containsNamed(ref) {
560
- switch (ref.kind) {
561
- case "scalar":
562
- return false;
563
- case "named":
564
- return true;
565
- case "opt":
566
- return containsNamed(ref.inner);
567
- case "arr":
568
- return containsNamed(ref.elem);
569
- case "map":
570
- return containsNamed(ref.value);
571
- }
572
- }
573
- /**
574
- * assertDeBoxableResults — fail closed (never a silent trust) for a covered node whose de-boxable result
575
- * is a NON-named top (arr / opt / map) that transitively contains a record: the generated de-box decodes
576
- * records only under a NAMED top (decode_wire_* / decode_wire_elem_*), so a record inside a non-named top
577
- * would be materialized on the trusting `.val` path and silently default its wire fields. That is the
578
- * silent-default red line forbids, so it is a loud generation-time error (native-codegen-standard
579
- * §2). A NAMED top (de-boxed) or a pure-scalar non-named top (no records to de-box) is fine.
580
- */
581
- /** readsAPriorNode — a componentRef node is a DATAFLOW consumer iff at least one input port is a ref
582
- * headed by a PRIOR BODY NODE (it reads a prior node's already-native result, #129), rather than a fresh
583
- * external wire read whose ports come from input params / static / an element ($as) binding. Same
584
- * head-is-a-body-node signal priorNodeArrElemInfo (#129) uses to lower a prior-node array feed. */
585
- function readsAPriorNode(node, typedNodes) {
586
- for (const expr of Object.values(node.ports ?? {})) {
587
- const e = classifyExpr(expr);
588
- if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0]))
589
- return true;
590
- }
591
- return false;
592
- }
593
- function assertDeBoxableResults(comp, typedNodes, plan) {
594
- for (const n of comp.body) {
595
- if ("cond" in n)
596
- continue;
597
- const isMapFanout = "map" in n || "fanout" in n;
598
- const ref = isMapFanout ? mapFanoutElemRef(n, typedNodes, plan) : typedNodes.get(n.id);
599
- const deBoxed = isMapFanout ? ref.kind === "named" : recordTop(ref) !== null;
600
- if (deBoxed)
601
- continue;
602
- const nodeId = n.id;
603
- if (containsNamed(ref)) {
604
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: node '${nodeId}' shape ${JSON.stringify(ref)} contains a record the de-box does not decode (a deeper nesting than a direct named / arr / opt / map of a named record). It would trust the record's wire fields (silent default) — the silent-default red line forbids. Fail closed: reshape so the record is a direct de-box top, or extend the recursion for this nesting.`);
605
- }
606
- // a map/fanout element is ALWAYS a per-element wire read; only a named record is de-boxed
607
- // (decode_wire_elem_*). A scalar / scalar-collection element materializes from the trusting per-element
608
- // path — a wrong wire value silent-defaults. Fail closed.
609
- if (isMapFanout) {
610
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: map/fanout node '${nodeId}' element ${JSON.stringify(ref)} is a scalar / scalar-collection per-element wire read that materializes on the trusting typed path — a wrong wire value would silent-default (diverging from run_behavior's outType check). Fail closed: declare the element as a record (de-boxed).`);
611
- }
612
- // a pure-scalar / scalar-collection top on a HANDLER-BACKED componentRef wire read trusts the typed
613
- // materialization (a wrong wire value silent-defaults). A DATAFLOW consumer (a port fed by a prior
614
- // node) computes from already-native data and generates.
615
- if (!readsAPriorNode(n, typedNodes)) {
616
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: node '${nodeId}' result top ${JSON.stringify(ref)} is a handler-backed scalar / scalar-collection read that materializes on the trusting typed path — a wrong wire value would silent-default (diverging from run_behavior's outType check). Fail closed: declare the result as a record (de-boxed) or feed it as dataflow from a prior node.`);
617
- }
618
- }
619
- }
620
386
  function staticArrElems(node) {
621
387
  if (node === null || typeof node !== "object" || Array.isArray(node))
622
388
  return null;
@@ -971,72 +737,6 @@ fn bc_expr_str_gt(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) == std::cmp::
971
737
  #[allow(dead_code)]
972
738
  fn bc_expr_str_ge(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) != std::cmp::Ordering::Less }`;
973
739
  }
974
- // ── CONCRETE per-node row structs + row→outType copiers (mirror the Go concrete contract) ──────────
975
- //
976
- // Each covered node has a CONCRETE row struct `RawRowNR<Comp><Node>` whose fields are the node's projected
977
- // outType fields (typed, flattened). A row models a SUCCESS: failure travels the Result's Err arm as a
978
- // concrete BehaviorError carrying the leaf's structured Error Value. A named-struct outType
979
- // flattens to one Rust field per outType field; a scalar/arr/opt node carries a single `val: <typed>`
980
- // payload. The per-component `HandlerNR<Comp>` trait has one method per node returning that concrete
981
- // struct; the runner reads `row.<field>` DIRECTLY (no `RawValue`, no `match`, no `Value::`) and copies
982
- // each field into the node's outType struct local.
983
- /** emitRawRowStructs — emit the concrete `RawRowNR<Comp><Node>` struct per covered node (+ per-element
984
- * `RawElemNR<Comp><Node>` for a covered map). A row models a SUCCESS; failure is the Result's Err arm. */
985
- function emitRawRowStructs(comp, typedNodes, plan) {
986
- const parts = [];
987
- for (const n of comp.body) {
988
- if ("cond" in n)
989
- continue; // #108: a cond node has no handler-result row (pure Expression).
990
- const ref = typedNodes.get(n.id);
991
- if ("fanout" in n) {
992
- const elemRowRef = fanoutItemsElemRef(n, ref, plan);
993
- parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
994
- parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
995
- `#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
996
- continue;
997
- }
998
- if ("map" in n) {
999
- const arrRef = ref;
1000
- const elemRowRef = mapElemRowRef(n, arrRef, plan);
1001
- parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
1002
- parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
1003
- `#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
1004
- continue;
1005
- }
1006
- parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
1007
- }
1008
- return parts.join("\n\n");
1009
- }
1010
- /** Emit one concrete row struct: flatten a named outType to typed fields, else a single `val` payload.
1011
- * A row models a SUCCESS — failure is the Result's Err arm, so no in-band error signal. */
1012
- function emitOneRowStruct(name, ref, plan) {
1013
- const lines = [];
1014
- lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType).`);
1015
- lines.push(`#[derive(Default)]`);
1016
- lines.push(`pub struct ${name} {`);
1017
- if (ref.kind === "named") {
1018
- const decl = findDecl(plan, ref.name);
1019
- for (const f of decl.fields) {
1020
- lines.push(` pub ${rustFieldName(f.name)}: ${renderTypeRef(f.type)}, // ${JSON.stringify(f.name)}`);
1021
- }
1022
- }
1023
- else {
1024
- lines.push(` pub val: ${renderTypeRef(ref)},`);
1025
- }
1026
- lines.push(`}`);
1027
- return lines.join("\n");
1028
- }
1029
- /** emitRowCopy — the statement copying a concrete row's typed fields into the node's outType local
1030
- * `target` (a value of Rust type renderTypeRef(ref)). Named: struct literal from the row's moved fields;
1031
- * scalar/arr/opt: `.val`. `rowExpr` is the concrete row VALUE (moved). No dynamic read — pure move/copy. */
1032
- function emitRowMove(target, rowExpr, ref, plan) {
1033
- if (ref.kind === "named") {
1034
- const decl = findDecl(plan, ref.name);
1035
- const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: ${rowExpr}.${rustFieldName(f.name)}`).join(", ");
1036
- return `${target} = ${ref.name} { ${inits} };`;
1037
- }
1038
- return `${target} = ${rowExpr}.val;`;
1039
- }
1040
740
  /**
1041
741
  * boundRustType (change #3 — typed `bound`) — the CONCRETE Rust type of a covered node's `bound` handler
1042
742
  * param (bc#90 / runtime-free). `bound` is the parent-produced key (the bindField value); it replaces the
@@ -1084,23 +784,19 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
1084
784
  const fnKwFor = (nodeId) => (ap.nodeIsAsync(nodeId) ? "async fn" : "fn");
1085
785
  const lines = [];
1086
786
  lines.push(`// ${handlerTraitName(comp.name)} — the CONCRETE per-component handler seam: one typed method`);
1087
- lines.push(`// per covered node (native ports struct IN, concrete row struct OUT). No generic boxed-ports`);
1088
- lines.push(`// / boxed-value / dynamic accessor crosses the covered boundary — the consumer implements each`);
1089
- lines.push(`// node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
787
+ lines.push(`// per covered node (native ports struct IN, WIRE value OUT). No generic boxed-ports / boxed-value`);
788
+ lines.push(`// / dynamic accessor crosses the covered boundary — the consumer implements each node_* by`);
789
+ lines.push(`// returning its own wire payload as a WireValue; the generated inline de-box turns that into the`);
790
+ lines.push(`// node's concrete native result (see INTEGRATION.md §6).`);
1090
791
  lines.push(`pub trait ${handlerTraitName(comp.name)} {`);
1091
- // a record-read node returns the consumer's WIRE (an associated `Wire: WireRow`); the generated
1092
- // decode turns it into the concrete row. Declared once when the component has a de-box-eligible node.
1093
- // The `+ Send` bound lets the wire cross the parallel-stage runner's scoped fan-out threads (the old
1094
- // concrete row was Send; the associated type needs the bound stated explicitly).
1095
- const hasDeBox = comp.body.some((n) => {
1096
- if ("cond" in n)
1097
- return false;
1098
- if ("map" in n || "fanout" in n)
1099
- return mapFanoutElemDeBoxEligible(n, typedNodes, plan);
1100
- return deBoxEligible(typedNodes.get(n.id));
1101
- });
1102
- if (hasDeBox)
1103
- lines.push(` type Wire: WireRow + Send;`);
792
+ // every covered node returns the consumer's WIRE (a single associated `Wire: WireValue`) the producer's
793
+ // datum at the node's result-consumption point; the generated inline de-box classifies it against the
794
+ // STATICALLY declared type. A batched map returns one wire per element (Vec); a fanout returns the aligned
795
+ // per-id wires (Vec<Option<Wire>> None = a DANGLING id). The `+ Send` bound lets the wire cross the
796
+ // parallel-stage runner's scoped fan-out threads.
797
+ const hasHandler = comp.body.some((n) => !("cond" in n));
798
+ if (hasHandler)
799
+ lines.push(` type Wire: WireValue + Send;`);
1104
800
  for (const n of comp.body) {
1105
801
  if ("cond" in n)
1106
802
  continue; // #108: a cond node has no handler method (pure Expression).
@@ -1108,29 +804,23 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
1108
804
  const fnKw = fnKwFor(n.id);
1109
805
  const bt = boundRustType(n, typedNodes, plan);
1110
806
  if ("fanout" in n) {
1111
- // fanout (v3): one batched dispatch of the deduped id list. A record element returns the aligned
1112
- // element WIRES the runner de-boxes (decode_wire_elem_*) then dedupe/drops. Each aligned position is
1113
- // Option<Self::Wire> — None = a DANGLING id (the runner maps it to the zero elem row).
807
+ // fanout (v3): one batched dispatch of the deduped id list, returning the aligned element WIRES the
808
+ // runner de-boxes then dedupe/drops. Each aligned position is Option<Self::Wire> — None = a DANGLING
809
+ // id (the runner maps it to the zero elem row).
1114
810
  const portsT = `${portsStructName(comp.name, n.id)}Batch`;
1115
- const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
1116
- const retT = elemDeBox ? "Vec<Option<Self::Wire>>" : rawRowStructName(comp.name, n.id);
1117
- lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Result<${retT}, BehaviorError>;`);
811
+ lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Result<Vec<Option<Self::Wire>>, BehaviorError>;`);
1118
812
  continue;
1119
813
  }
1120
814
  if ("map" in n) {
1121
815
  const m = n.map;
1122
816
  const batched = m.batched === true;
1123
817
  const portsT = batched ? `${portsStructName(comp.name, n.id)}Batch` : portsStructName(comp.name, n.id);
1124
- // a record-element map returns the element WIRE(s): batched → Vec<Self::Wire>, nonbatched → Self::Wire.
1125
- const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
1126
- const retT = elemDeBox ? (batched ? "Vec<Self::Wire>" : "Self::Wire") : batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
818
+ // a map returns the element WIRE(s): batched → Vec<Self::Wire>, non-batched → Self::Wire.
819
+ const retT = batched ? "Vec<Self::Wire>" : "Self::Wire";
1127
820
  lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Result<${retT}, BehaviorError>;`);
1128
821
  continue;
1129
822
  }
1130
- const ref = typedNodes.get(n.id);
1131
- const top = ref !== undefined ? recordTop(ref) : null;
1132
- const retT = top !== null ? rustWireTypeForTop(top) : rawRowStructName(comp.name, n.id);
1133
- lines.push(` ${fnKw} ${method}(&self, ports: &${portsStructName(comp.name, n.id)}, bound: ${bt}) -> Result<${retT}, BehaviorError>;`);
823
+ lines.push(` ${fnKw} ${method}(&self, ports: &${portsStructName(comp.name, n.id)}, bound: ${bt}) -> Result<Self::Wire, BehaviorError>;`);
1134
824
  }
1135
825
  lines.push(`}`);
1136
826
  return lines.join("\n");
@@ -1172,9 +862,8 @@ function mapElemRowRef(node, arrRef, plan) {
1172
862
  }
1173
863
  /**
1174
864
  * emitMapElemMaterializers — for each covered map...into node, emit the AUGMENTED-element copier: copy
1175
- * the over element's typed fields + move the `into` field from the concrete per-element handler row
1176
- * (RawElemNR). Pure struct field assignment — NO dynamic value read (the handler already produced the
1177
- * typed into row).
865
+ * the over element's typed fields + place the already-de-boxed `into` value into its field. Pure struct
866
+ * field assignment — NO dynamic value read (the inline de-box already produced the typed `into` value).
1178
867
  */
1179
868
  function emitMapElemMaterializers(comp, typedNodes, plan) {
1180
869
  const parts = [];
@@ -1196,7 +885,6 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
1196
885
  const overFields = decl.fields.filter((f) => f.name !== intoKey);
1197
886
  const overElemTy = mapOverElemType(comp, n, typedNodes, plan);
1198
887
  const fn = mapElemMaterializerName(comp.name, n.id);
1199
- const elemRowGo = rawElemStructName(comp.name, n.id);
1200
888
  const intoRowRef = mapElemRowRef(n, arrRef, plan);
1201
889
  const inits = [];
1202
890
  // Copy scalars (i64/f64/bool) copy directly; String/named/arr/opt clone (clippy clone_on_copy clean).
@@ -1204,15 +892,12 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
1204
892
  const isCopy = f.type.kind === "scalar" && (f.type.scalar === "int" || f.type.scalar === "float" || f.type.scalar === "bool");
1205
893
  inits.push(` ${rustFieldName(f.name)}: over.${rustFieldName(f.name)}${isCopy ? "" : ".clone()"},`);
1206
894
  }
1207
- // move the into field off the concrete element row (named struct literal; scalar/arr/opt .val).
1208
- const intoValue = intoRowRef.kind === "named"
1209
- ? `${intoRowRef.name} { ${findDecl(plan, intoRowRef.name).fields.map((f) => `${rustFieldName(f.name)}: into.${rustFieldName(f.name)}`).join(", ")} }`
1210
- : `into.val`;
895
+ // the `into` param is the already-de-boxed into value (its native type) drop it straight in.
1211
896
  parts.push(`#[allow(dead_code)]
1212
- fn ${fn}(over: &${overElemTy}, into: ${elemRowGo}) -> ${elemRef.name} {
897
+ fn ${fn}(over: &${overElemTy}, into: ${renderTypeRef(intoRowRef)}) -> ${elemRef.name} {
1213
898
  ${elemRef.name} {
1214
899
  ${inits.join("\n")}
1215
- ${rustFieldName(intoKey)}: ${intoValue},
900
+ ${rustFieldName(intoKey)}: into,
1216
901
  }
1217
902
  }`);
1218
903
  void intoField;
@@ -1739,11 +1424,11 @@ function emitNativeRawRunner(comp, plan, ap) {
1739
1424
  const lines = [];
1740
1425
  lines.push(`// run_native_raw_struct_${fn} — the STRUCT-RETURNING combined read (bc#77/#87/#94): the fully`);
1741
1426
  lines.push(`// de-plumbed CONCRETE path. Generic over the per-component CONCRETE ${handlerTraitName(comp.name)} trait,`);
1742
- lines.push(`// whose node_* methods take the node's native ports struct and return the concrete per-node row`);
1743
- lines.push(`// struct (typed fields = the outType). The runner builds each ports struct by direct native`);
1744
- lines.push(`// construction (a simple string port lowers to a native Rust expr), dispatches the concrete`);
1745
- lines.push(`// node_* method, and reads row.<field> DIRECTLY into the node's outType struct cell — no boxed`);
1746
- lines.push(`// handler result, no generic enum crossing, no dispatch on a dynamic value on the covered plane. Node`);
1427
+ lines.push(`// whose node_* methods take the node's native ports struct and return the node's result WIRE`);
1428
+ lines.push(`// (Self::Wire: WireValue). The runner builds each ports struct by direct native construction (a`);
1429
+ lines.push(`// simple string port lowers to a native Rust expr), dispatches the concrete node_* method, and`);
1430
+ lines.push(`// de-boxes the result wire INLINE (match wire.as_*() { } fully unrolled — no decode helper) into`);
1431
+ lines.push(`// the node's outType struct cell — no boxed handler result, no generic enum crossing, no dispatch on a dynamic value on the covered plane. Node`);
1747
1432
  lines.push(`// results are typed struct cells; a relation child reads the parent's REAL struct result via`);
1748
1433
  lines.push(`// direct field access (child-present decision from the real parent value — relationSingle /`);
1749
1434
  lines.push(`// connection converge). A real-concurrency stage (bc#87) is static parallel orchestration —`);
@@ -1847,57 +1532,33 @@ function emitNativeRawRunner(comp, plan, ap) {
1847
1532
  // directly (no `.await`). The runner being `async fn` does not force every call site to await.
1848
1533
  const awaitSfx = ap.nodeIsAsync(node.id) ? ".await" : "";
1849
1534
  lines.push(`${indent}let ports_${s} = ${structName} { ${inits.join(", ")} };`);
1850
- // a de-box-eligible node returns the wire (Self::Wire / Vec / Option / BTreeMap); the generated
1851
- // decode_wire_result_* probes each record strictly and produces the structured error on a mismatch
1852
- // (byte-equal codes to run_behavior's outType check, so ≡ holds). A non-de-box node keeps its direct
1853
- // concrete-row move.
1854
- const top = recordTop(ref);
1855
- const namedTop = top !== null && top.kind === "named";
1856
- const deBox = top !== null;
1535
+ // uniform seam: every covered node returns a single result WIRE (Self::Wire); the inline de-box
1536
+ // (renderResultDeBox) classifies it against the STATICALLY declared result type and produces the
1537
+ // structured Error Value on a mismatch (byte-equal codes to run_behavior's outType check, so ≡ holds).
1857
1538
  if (meta.policy === "continue") {
1858
- // continue: a failed node (leaf failure OR de-box mismatch) produces no Port — the downstream
1859
- // Skip propagation is what `continue` means. Only a clean decode commits the result.
1860
- if (namedTop) {
1861
- lines.push(`${indent}if let Ok(wire_${s}) = handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
1862
- lines.push(`${indent} if let Ok(row_${s}) = ${rustDecodeNamedName(top.innerName)}(&wire_${s}) {`);
1863
- lines.push(`${indent} ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1864
- lines.push(`${indent} produced_${s}.set(true);`);
1865
- lines.push(`${indent} }`);
1866
- lines.push(`${indent}}`);
1867
- }
1868
- else if (deBox) {
1869
- lines.push(`${indent}if let Ok(wire_${s}) = handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
1870
- lines.push(`${indent} if let Ok(res_${s}) = ${rustDecodeResultName(comp.name, node.id)}(wire_${s}) {`);
1871
- lines.push(`${indent} *${typedCell(node.id)}.borrow_mut() = res_${s};`);
1872
- lines.push(`${indent} produced_${s}.set(true);`);
1873
- lines.push(`${indent} }`);
1874
- lines.push(`${indent}}`);
1875
- }
1876
- else {
1877
- lines.push(`${indent}if let Ok(row_${s}) = handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
1878
- lines.push(`${indent} ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1879
- lines.push(`${indent} produced_${s}.set(true);`);
1880
- lines.push(`${indent}}`);
1881
- }
1539
+ // continue: a failed node (leaf failure OR de-box mismatch) produces no Port — the downstream Skip
1540
+ // propagation is what `continue` means. The inline de-box's mismatch breaks the labeled block (a
1541
+ // local skip, NOT a runner-wide propagate); only a clean decode commits. (Covered componentRef nodes
1542
+ // are always 'fail' policy today — this arm is defensive.)
1543
+ const label = `debox_${s}`;
1544
+ const contFail = (e) => `break '${label} Err(${e})`;
1545
+ lines.push(`${indent}if let Ok(wire_${s}) = handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
1546
+ lines.push(`${indent} let decoded_${s}: Result<${renderTypeRef(ref)}, BehaviorError> = '${label}: {`);
1547
+ lines.push(`${indent} Ok(${renderResultDeBox(ref, `wire_${s}`, node.id, plan, `${indent} `, contFail)})`);
1548
+ lines.push(`${indent} };`);
1549
+ lines.push(`${indent} if let Ok(val_${s}) = decoded_${s} {`);
1550
+ lines.push(`${indent} *${typedCell(node.id)}.borrow_mut() = val_${s};`);
1551
+ lines.push(`${indent} produced_${s}.set(true);`);
1552
+ lines.push(`${indent} }`);
1553
+ lines.push(`${indent}}`);
1882
1554
  }
1883
1555
  else {
1884
1556
  const policyLit = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1885
- const resultVar = deBox ? `wire_${s}` : `row_${s}`;
1886
- lines.push(`${indent}let ${resultVar} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
1557
+ lines.push(`${indent}let wire_${s} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
1887
1558
  lines.push(`${indent} Ok(r) => r,`);
1888
1559
  lines.push(`${indent} Err(e) => return Err(BehaviorError { code: "OP_FAILED".to_string(), message: format!("operation '{}' failed under '${policyLit}: {}", ${rustStrLit(node.id)}, e.message), detail: e.detail }),`);
1889
1560
  lines.push(`${indent}};`);
1890
- if (namedTop) {
1891
- lines.push(`${indent}let row_${s} = ${rustDecodeNamedName(top.innerName)}(&wire_${s})?;`);
1892
- lines.push(`${indent}${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1893
- }
1894
- else if (deBox) {
1895
- lines.push(`${indent}let res_${s} = ${rustDecodeResultName(comp.name, node.id)}(wire_${s})?;`);
1896
- lines.push(`${indent}*${typedCell(node.id)}.borrow_mut() = res_${s};`);
1897
- }
1898
- else {
1899
- lines.push(`${indent}${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1900
- }
1561
+ lines.push(`${indent}*${typedCell(node.id)}.borrow_mut() = ${renderResultDeBox(ref, `wire_${s}`, node.id, plan, indent)};`);
1901
1562
  lines.push(`${indent}produced_${s}.set(true);`);
1902
1563
  }
1903
1564
  for (const c of closers)
@@ -2023,25 +1684,11 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
2023
1684
  const boundN = Math.min(concurrency, nMembers);
2024
1685
  const jobsVar = `jobs_${sanitize(comp.name)}`;
2025
1686
  const hWire = `<H as ${handlerTraitName(comp.name)}>::Wire`;
2026
- const hWireTop = (top) => {
2027
- switch (top.kind) {
2028
- case "named":
2029
- return hWire;
2030
- case "arr":
2031
- return `Vec<${hWire}>`;
2032
- case "opt":
2033
- return `Option<${hWire}>`;
2034
- case "map":
2035
- return `std::collections::BTreeMap<String, ${hWire}>`;
2036
- }
2037
- };
2038
1687
  for (const i of refMembers) {
2039
1688
  const s = sanitize(comp.body[i].id);
2040
- // a de-box member's thread returns the WIRE (shaped by the record top); a non-de-box member returns
2041
- // its concrete row directly.
2042
- const memTop = recordTop(typedNodes.get(comp.body[i].id));
2043
- const slotT = memTop !== null ? hWireTop(memTop) : rawRowStructName(comp.name, comp.body[i].id);
2044
- lines.push(` let slot_${s}: std::sync::Mutex<Option<Result<${slotT}, BehaviorError>>> = std::sync::Mutex::new(None);`);
1689
+ // every member's thread returns the result WIRE (a single Self::Wire); the ascending COMMIT phase
1690
+ // de-boxes it (so the wire — not the decode — is what crosses the scoped threads).
1691
+ lines.push(` let slot_${s}: std::sync::Mutex<Option<Result<${hWire}, BehaviorError>>> = std::sync::Mutex::new(None);`);
2045
1692
  }
2046
1693
  lines.push(` let mut ${jobsVar}: Vec<usize> = Vec::new();`);
2047
1694
  refMembers.forEach((i, slot) => {
@@ -2080,51 +1727,28 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
2080
1727
  const ref = typedNodes.get(node.id);
2081
1728
  lines.push(` if ports_${s}.is_some() {`);
2082
1729
  lines.push(` let out_${s} = slot_${s}.lock().unwrap().take().expect("runnable member wrote its slot");`);
2083
- const top = recordTop(ref);
2084
- const namedTop = top !== null && top.kind === "named";
2085
- const deBox = top !== null;
2086
1730
  if (meta.policy === "continue") {
2087
- if (namedTop) {
2088
- lines.push(` if let Ok(wire_${s}) = out_${s} {`);
2089
- lines.push(` if let Ok(row_${s}) = ${rustDecodeNamedName(top.innerName)}(&wire_${s}) {`);
2090
- lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
2091
- lines.push(` produced_${s}.set(true);`);
2092
- lines.push(` }`);
2093
- lines.push(` }`);
2094
- }
2095
- else if (deBox) {
2096
- lines.push(` if let Ok(wire_${s}) = out_${s} {`);
2097
- lines.push(` if let Ok(res_${s}) = ${rustDecodeResultName(comp.name, node.id)}(wire_${s}) {`);
2098
- lines.push(` *${typedCell(node.id)}.borrow_mut() = res_${s};`);
2099
- lines.push(` produced_${s}.set(true);`);
2100
- lines.push(` }`);
2101
- lines.push(` }`);
2102
- }
2103
- else {
2104
- lines.push(` if let Ok(row_${s}) = out_${s} {`);
2105
- lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
2106
- lines.push(` produced_${s}.set(true);`);
2107
- lines.push(` }`);
2108
- }
1731
+ // continue: a failed member (leaf failure OR de-box mismatch) commits no Port (defensive — covered
1732
+ // componentRef members are always 'fail' policy today).
1733
+ const label = `debox_${s}`;
1734
+ const contFail = (e) => `break '${label} Err(${e})`;
1735
+ lines.push(` if let Ok(wire_${s}) = out_${s} {`);
1736
+ lines.push(` let decoded_${s}: Result<${renderTypeRef(ref)}, BehaviorError> = '${label}: {`);
1737
+ lines.push(` Ok(${renderResultDeBox(ref, `wire_${s}`, node.id, plan, " ", contFail)})`);
1738
+ lines.push(` };`);
1739
+ lines.push(` if let Ok(val_${s}) = decoded_${s} {`);
1740
+ lines.push(` *${typedCell(node.id)}.borrow_mut() = val_${s};`);
1741
+ lines.push(` produced_${s}.set(true);`);
1742
+ lines.push(` }`);
1743
+ lines.push(` }`);
2109
1744
  }
2110
1745
  else {
2111
1746
  const policyLit = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
2112
- const resultVar = deBox ? `wire_${s}` : `row_${s}`;
2113
- lines.push(` let ${resultVar} = match out_${s} {`);
1747
+ lines.push(` let wire_${s} = match out_${s} {`);
2114
1748
  lines.push(` Ok(r) => r,`);
2115
1749
  lines.push(` Err(e) => return Err(BehaviorError { code: "OP_FAILED".to_string(), message: format!("operation '{}' failed under '${policyLit}: {}", ${rustStrLit(node.id)}, e.message), detail: e.detail }),`);
2116
1750
  lines.push(` };`);
2117
- if (namedTop) {
2118
- lines.push(` let row_${s} = ${rustDecodeNamedName(top.innerName)}(&wire_${s})?;`);
2119
- lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
2120
- }
2121
- else if (deBox) {
2122
- lines.push(` let res_${s} = ${rustDecodeResultName(comp.name, node.id)}(wire_${s})?;`);
2123
- lines.push(` *${typedCell(node.id)}.borrow_mut() = res_${s};`);
2124
- }
2125
- else {
2126
- lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
2127
- }
1751
+ lines.push(` *${typedCell(node.id)}.borrow_mut() = ${renderResultDeBox(ref, `wire_${s}`, node.id, plan, " ")};`);
2128
1752
  lines.push(` produced_${s}.set(true);`);
2129
1753
  }
2130
1754
  lines.push(` }`);
@@ -2161,11 +1785,11 @@ isAsync = false) {
2161
1785
  const portNames = Object.keys(m.ports);
2162
1786
  const elemArrRef = typedNodes.get(node.id);
2163
1787
  const elemName = renderTypeRef(elemArrRef.elem);
2164
- // a record element is de-boxed strictly: the handler returns the element WIRE(s) and the arm decodes via
2165
- // decode_wire_elem_* (closing the scan-row hole). A de-box mismatch fails closed regardless of
2166
- // elementPolicy (matching the interpreter's whole-node outType throw).
2167
- const elemDeBox = mapFanoutElemDeBoxEligible(node, typedNodes, plan);
2168
- const decElemFn = elemDeBox ? rustDecodeElemName(comp.name, node.id) : "";
1788
+ // each element is de-boxed strictly INLINE: the handler returns the per-element WIRE and the arm decodes
1789
+ // it against the element ref (the `into` field's type for map...into, else the element type). A de-box
1790
+ // mismatch fails closed regardless of elementPolicy (matching the interpreter's whole-node outType throw).
1791
+ const elemDeBoxRef = mapElemRowRef(node, elemArrRef, plan);
1792
+ const renderElemDeBox = (il) => renderResultDeBox(elemDeBoxRef, `er_${s}`, node.id, plan, il);
2169
1793
  // build the per-element CONCRETE native ports struct. `il` = indent inside the element loop.
2170
1794
  const buildPorts = (il) => {
2171
1795
  const out = [];
@@ -2225,28 +1849,23 @@ isAsync = false) {
2225
1849
  lines.push(`${indent} Ok(r) => r,`);
2226
1850
  lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
2227
1851
  lines.push(`${indent} };`);
2228
- const batchLen = elemDeBox ? `row_${s}.len()` : `row_${s}.rows.len()`;
2229
- const batchIter = elemDeBox ? `row_${s}.into_iter()` : `row_${s}.rows.into_iter()`;
2230
- lines.push(`${indent} if ${batchLen} != want_${s} {`);
1852
+ lines.push(`${indent} if row_${s}.len() != want_${s} {`);
2231
1853
  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})));`);
2232
1854
  lines.push(`${indent} }`);
2233
1855
  if (hasInto) {
2234
- // zip the aligned batch rows onto each KEPT over element (into materializer). A record element's
2235
- // wire is de-boxed strictly first.
2236
- lines.push(`${indent} let mut rows_iter_${s} = ${batchIter};`);
1856
+ // zip the aligned per-element WIRES onto each KEPT over element: de-box the element wire strictly
1857
+ // (into the `into` field's type) then materialize the augmented element.
1858
+ lines.push(`${indent} let mut rows_iter_${s} = row_${s}.into_iter();`);
2237
1859
  lines.push(`${indent} for over_el_${s} in kept_${s}.iter() {`);
2238
1860
  lines.push(`${indent} let er_${s} = rows_iter_${s}.next().expect("aligned batch row");`);
2239
- if (elemDeBox)
2240
- lines.push(`${indent} let er_${s} = ${decElemFn}(&er_${s})?;`);
2241
- lines.push(`${indent} built_${s}.push(${elemMat}(over_el_${s}, er_${s}));`);
1861
+ lines.push(`${indent} let dec_${s} = ${renderElemDeBox(`${indent} `)};`);
1862
+ lines.push(`${indent} built_${s}.push(${elemMat}(over_el_${s}, dec_${s}));`);
2242
1863
  lines.push(`${indent} }`);
2243
1864
  }
2244
1865
  else {
2245
- // no-into: collect each element DIRECTLY from the aligned per-element row (result = kept rows).
2246
- lines.push(`${indent} for er_${s} in ${batchIter} {`);
2247
- if (elemDeBox)
2248
- lines.push(`${indent} let er_${s} = ${decElemFn}(&er_${s})?;`);
2249
- lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
1866
+ // no-into: de-box each aligned per-element wire DIRECTLY into the element (result = kept elements).
1867
+ lines.push(`${indent} for er_${s} in row_${s}.into_iter() {`);
1868
+ lines.push(`${indent} let el_${s} = ${renderElemDeBox(`${indent} `)};`);
2250
1869
  lines.push(`${indent} built_${s}.push(el_${s});`);
2251
1870
  lines.push(`${indent} }`);
2252
1871
  }
@@ -2271,16 +1890,14 @@ isAsync = false) {
2271
1890
  lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
2272
1891
  }
2273
1892
  lines.push(`${indent} };`);
2274
- // a record element's wire is de-boxed strictly (fail-closed regardless of elementPolicy).
2275
- if (elemDeBox)
2276
- lines.push(`${indent} let er_${s} = ${decElemFn}(&er_${s})?;`);
1893
+ // the element's wire is de-boxed strictly INLINE (fail-closed regardless of elementPolicy).
1894
+ lines.push(`${indent} let dec_${s} = ${renderElemDeBox(`${indent} `)};`);
2277
1895
  if (hasInto) {
2278
- lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s}, er_${s}));`);
1896
+ lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s}, dec_${s}));`);
2279
1897
  }
2280
1898
  else {
2281
1899
  lines.push(`${indent} let _ = &oel_${s};`);
2282
- lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
2283
- lines.push(`${indent} built_${s}.push(el_${s});`);
1900
+ lines.push(`${indent} built_${s}.push(dec_${s});`);
2284
1901
  }
2285
1902
  lines.push(`${indent}}`);
2286
1903
  }
@@ -2349,11 +1966,9 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
2349
1966
  lines.push(`${indent} Ok(r) => r,`);
2350
1967
  lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
2351
1968
  lines.push(`${indent} };`);
2352
- // the fanout element row is a named record the handler returns the aligned element WIRES
2353
- // (Vec<Option<Self::Wire>>) and each present body is de-boxed strictly (a de-box mismatch fails closed).
2354
- // A DANGLING id is None → the zero element row (empty dedupeKey), which the dangling drop excludes.
2355
- const decElemFn = rustDecodeElemName(comp.name, node.id);
2356
- const rawElemT = rawElemStructName(comp.name, node.id);
1969
+ // the handler returns the aligned per-id element WIRES (Vec<Option<Self::Wire>>); each present body is
1970
+ // de-boxed strictly INLINE (a de-box mismatch fails closed). A DANGLING id is None → the zero element
1971
+ // (empty dedupeKey), which the dangling drop excludes.
2357
1972
  lines.push(`${indent} if row_${s}.len() != want_${s} {`);
2358
1973
  lines.push(`${indent} return Err(BehaviorError::new("FANOUT_BATCH_RESULT_MISMATCH", format!("fanout '{}': batched handler must return a list aligned to the deduped id list (want {})", ${rustStrLit(node.id)}, want_${s})));`);
2359
1974
  lines.push(`${indent} }`);
@@ -2362,11 +1977,10 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
2362
1977
  // structural (the elem type already omits it). cursor is always None (connection wrap).
2363
1978
  lines.push(`${indent} let mut seen_${s}: std::collections::HashSet<String> = std::collections::HashSet::new();`);
2364
1979
  lines.push(`${indent} for opt_er_${s} in row_${s}.into_iter() {`);
2365
- lines.push(`${indent} let er_${s}: ${rawElemT} = match opt_er_${s} {`);
2366
- lines.push(`${indent} Some(wire_${s}) => ${decElemFn}(&wire_${s})?,`);
1980
+ lines.push(`${indent} let el_${s}: ${elemName} = match opt_er_${s} {`);
1981
+ lines.push(`${indent} Some(wire_${s}) => ${renderResultDeBox(elemRef, `wire_${s}`, node.id, plan, `${indent} `)},`);
2367
1982
  lines.push(`${indent} None => Default::default(),`);
2368
1983
  lines.push(`${indent} };`);
2369
- lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemRef, plan)}`);
2370
1984
  lines.push(`${indent} let key_${s} = el_${s}.${dkRust}.clone();`);
2371
1985
  if (f.drop === "dangling") {
2372
1986
  lines.push(`${indent} if key_${s}.is_empty() { continue; }`);
@@ -2444,16 +2058,6 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
2444
2058
  lines.push(cl);
2445
2059
  return lines.join("\n");
2446
2060
  }
2447
- /** emitElemMove — move a concrete element row (RawElemNR, error already handled) into a fresh `let` local
2448
- * of the element outType. Named → struct literal from moved fields; scalar/arr/opt → .val. */
2449
- function emitElemMove(letBind, rowExpr, ref, plan) {
2450
- if (ref.kind === "named") {
2451
- const decl = findDecl(plan, ref.name);
2452
- const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: ${rowExpr}.${rustFieldName(f.name)}`).join(", ");
2453
- return `${letBind} = ${ref.name} { ${inits} };`;
2454
- }
2455
- return `${letBind} = ${rowExpr}.val;`;
2456
- }
2457
2061
  /**
2458
2062
  * emitProducedAwareValue — the #86 part 1 produced-aware output lowering (rust twin of the go one).
2459
2063
  */
@@ -2575,13 +2179,12 @@ ${emitStructSurface([])}
2575
2179
  // #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler).
2576
2180
  rustExprUsed.helpers = false;
2577
2181
  const structs = [];
2578
- const rowStructs = [];
2579
2182
  const handlerTraits = [];
2580
2183
  const inStructs = [];
2581
2184
  const elemMaterializers = [];
2582
- // the generated strict de-box functions (deduped by named type across all covered components).
2583
- const decodeFns = [];
2584
- const decodeEmitted = new Set();
2185
+ // every covered node returns a WireValue the inline de-box classifies the wire seam (probe enums +
2186
+ // WireValue/WireRow/WireList traits) is emitted once whenever any handler node exists.
2187
+ const anyWire = native.some((c) => c.body.some((n) => !("cond" in n)));
2585
2188
  for (const c of native) {
2586
2189
  const typedNodes = new Map();
2587
2190
  // build the full typed-node map FIRST so a map's over-element resolution can see every node.
@@ -2599,11 +2202,7 @@ ${emitStructSurface([])}
2599
2202
  else
2600
2203
  structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
2601
2204
  }
2602
- // fail closed for a record inside a non-named top (never a silent trust) — before emitting anything.
2603
- assertDeBoxableResults(c, typedNodes, plan);
2604
- rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
2605
2205
  handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
2606
- emitRustNodeDecodeFns(c, typedNodes, plan, decodeEmitted, decodeFns);
2607
2206
  inStructs.push(emitInStruct(c, plan));
2608
2207
  const mm = emitMapElemMaterializers(c, typedNodes, plan);
2609
2208
  if (mm)
@@ -2618,9 +2217,9 @@ ${emitStructSurface([])}
2618
2217
  // relationKind:single children), emitted ONLY as a STRUCT-returning runner GENERIC over a per-component
2619
2218
  // CONCRETE handler trait (HandlerNR<comp>): native ports struct IN (direct construction — every port is a
2620
2219
  // CONCRETE Rust value: String / bool / Vec<&str> projection / i64 limit — NO boxed Value, NO by-name
2621
- // accessor); the handler takes a typed produced-aware 'bound' Option and returns the node's CONCRETE row
2622
- // struct (typed fields = the projected outType), and the runner reads row.<field> DIRECTLY into the node's
2623
- // outType struct. The covered plane carries NO bc-runtime reference at all — failures use the LOCAL
2220
+ // accessor); the handler takes a typed produced-aware 'bound' Option and returns the node's result WIRE
2221
+ // (Self::Wire: WireValue), and the runner de-boxes it INLINE (fully-unrolled probe/match — no decode helper,
2222
+ // no output recursion) into the node's outType struct. The covered plane carries NO bc-runtime reference at all — failures use the LOCAL
2624
2223
  // BehaviorError (same codes, byte-equal to run_behavior). INLINE sequential (no plan driver) so a relation
2625
2224
  // child reads the parent's REAL struct result via direct field access and the child-present decision is
2626
2225
  // made from the real parent value — relationSingle / connection converge with run_behavior (fixes
@@ -2641,16 +2240,10 @@ use std::cell::RefCell;`;
2641
2240
  structs.some((r) => r.trim().length > 0)
2642
2241
  ? `// Native ports structs (one per componentRef node; typed per the static port type — CONCRETE).\n${structs.filter((r) => r.trim().length > 0).join("\n\n")}`
2643
2242
  : undefined,
2644
- rowStructs.some((r) => r.trim().length > 0)
2645
- ? `// CONCRETE per-node row structs (typed fields = the node's outType; + error signal).\n${rowStructs.filter((r) => r.trim().length > 0).join("\n\n")}`
2646
- : undefined,
2647
2243
  `// CONCRETE per-component input structs (fields = inputPorts).\n${inStructs.join("\n\n")}`,
2648
- `// CONCRETE per-component handler traits (one node_* method per node — native ports IN, row OUT).\n${handlerTraits.join("\n\n")}`,
2649
- decodeFns.length
2650
- ? `// strict de-box wire seam — the consumer implements WireRow/WireList over its wire payload (variant\n// classification); the generated decode_wire_* owns strictness (required/optional, present/absent, error\n// assembly, fail-closed). Concrete enums + monomorphized traits — no boxed value, no trait object, no\n// heap allocation crosses the seam.\n${emitRustProbeWireTypes()}`
2651
- : undefined,
2652
- decodeFns.length
2653
- ? `// Generated strict de-box functions (per named record type; structured Error Value on mismatch).\n${decodeFns.join("\n\n")}`
2244
+ `// CONCRETE per-component handler traits (one node_* method per node — native ports IN, WIRE value OUT).\n${handlerTraits.join("\n\n")}`,
2245
+ anyWire
2246
+ ? `// strict de-box wire seam — the consumer implements WireValue (+ WireRow/WireList reached via\n// as_row/as_list) over its wire payload (variant classification); the generated INLINE de-box owns\n// strictness (required/optional, present/absent, error assembly, fail-closed). Concrete enums +\n// monomorphized traits — no boxed value, no trait object, no heap allocation crosses the seam.\n${emitRustProbeWireTypes()}`
2654
2247
  : undefined,
2655
2248
  elemMaterializers.length
2656
2249
  ? `// map augmented-element copiers (pure struct field assignment — no dynamic value read).\n${elemMaterializers.join("\n\n")}`
@@ -2683,25 +2276,14 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
2683
2276
  const anyAsyncRunner = native.some((c) => asyncPlans.get(c.name).runnerIsAsync);
2684
2277
  // ── per-component input decoders: generic &[(String,Value)] -> concrete InNR_<comp> (test glue). ──
2685
2278
  const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
2686
- // does any covered read de-box a named record a componentRef record OR a map/fanout element row
2687
- // (→ the scripted handler returns a ScriptedWireRow / Vec<ScriptedWireRow>)?
2688
- const anyDeBox = native.some((c) => {
2689
- const tn = new Map();
2690
- for (const n of c.body)
2691
- if (!isControlGate(n))
2692
- tn.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
2693
- return c.body.some((n) => {
2694
- if ("cond" in n)
2695
- return false;
2696
- if ("map" in n || "fanout" in n)
2697
- return mapFanoutElemDeBoxEligible(n, tn, plan);
2698
- return deBoxEligible(tn.get(n.id));
2699
- });
2700
- });
2701
- // scripted wire adapter (TEST glue): wraps a scripted Value as a WireRow/WireList and classifies each
2702
- // attribute's native type into a DynamoDB-flavored wire tag (S/N/BOOL/L/M/NULL). A real consumer
2703
- // implements WireRow over its own AttributeValue map; this drives the generated de-box from the same
2704
- // scripted vectors run_behavior uses — so actualWireType observed in a mismatch is the PRODUCER tag.
2279
+ // every covered node returns a WireValue the inline de-box classifies so the scripted wire adapter is
2280
+ // needed whenever any component has a handler node.
2281
+ const anyDeBox = native.some((c) => c.body.some((n) => !("cond" in n)));
2282
+ // scripted wire adapter (TEST glue): wraps a scripted Value as a WireValue (+ WireRow/WireList reached
2283
+ // via as_row/as_list) and classifies each datum's native type into a DynamoDB-flavored wire tag
2284
+ // (S/N/BOOL/L/M/NULL). A real consumer implements WireValue over its own AttributeValue payload; this
2285
+ // drives the generated inline de-box from the same scripted vectors run_behavior uses — so the
2286
+ // actualWireType observed in a mismatch is the PRODUCER tag.
2705
2287
  const wireAdapter = anyDeBox
2706
2288
  ? `// ── scripted wire adapter (TEST glue) ──
2707
2289
  fn scripted_wire_tag(v: &Value) -> String {
@@ -2793,6 +2375,37 @@ pub struct ScriptedWireList {
2793
2375
  items: Vec<Value>,
2794
2376
  }
2795
2377
 
2378
+ // ScriptedWireValue — a scripted node result wrapped as the uniform WireValue: a top value is always
2379
+ // present (Some), so as_* never observes Absent; a scripted null is Value::Null → Probe::Null.
2380
+ pub struct ScriptedWireValue {
2381
+ v: Value,
2382
+ }
2383
+
2384
+ impl ScriptedWireValue {
2385
+ pub fn new(v: Value) -> Self {
2386
+ ScriptedWireValue { v }
2387
+ }
2388
+ }
2389
+
2390
+ impl WireValue for ScriptedWireValue {
2391
+ type Row = ScriptedWireRow;
2392
+ fn as_string(&self) -> Probe<String> {
2393
+ probe_string_val(Some(&self.v))
2394
+ }
2395
+ fn as_number(&self) -> NumProbe {
2396
+ probe_number_val(Some(&self.v))
2397
+ }
2398
+ fn as_bool(&self) -> Probe<bool> {
2399
+ probe_bool_val(Some(&self.v))
2400
+ }
2401
+ fn as_row(&self) -> Probe<ScriptedWireRow> {
2402
+ probe_row_val(Some(&self.v))
2403
+ }
2404
+ fn as_list(&self) -> Probe<ScriptedWireList> {
2405
+ probe_list_val(Some(&self.v))
2406
+ }
2407
+ }
2408
+
2796
2409
  impl WireRow for ScriptedWireRow {
2797
2410
  type List = ScriptedWireList;
2798
2411
  fn keys(&self) -> Vec<String> {
@@ -2837,10 +2450,8 @@ impl WireList for ScriptedWireList {
2837
2450
  }
2838
2451
  }`
2839
2452
  : "";
2840
- // ── concrete scripted handler: implements every HandlerNR<comp> trait by decoding a scripted Value
2841
- // into the concrete per-node row (the generic scripted-source queue meets the concrete seam). ──
2842
- const decodeFns = [];
2843
- const emittedDecode = new Set();
2453
+ // ── concrete scripted handler: implements every HandlerNR<comp> trait by returning the scripted ok
2454
+ // Value as a WireValue (the generic scripted-source queue meets the uniform wire seam). ──
2844
2455
  const traitImpls = [];
2845
2456
  for (const c of native) {
2846
2457
  const ap = asyncPlans.get(c.name);
@@ -2860,15 +2471,11 @@ impl WireList for ScriptedWireList {
2860
2471
  // Option<K> (NOT a boxed Value) — computed per node so the impl signature matches the trait.
2861
2472
  const bt = boundRustType(n, typedNodes, plan);
2862
2473
  if ("fanout" in n) {
2474
+ // fanout: return the aligned per-id element WIRES (Vec<Option<ScriptedWireValue>>); a null aligned
2475
+ // body is a DANGLING id → None (the covered runner maps it to the zero element).
2863
2476
  const f = n.fanout;
2864
- const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
2865
- const elemT = rawElemStructName(c.name, n.id);
2866
- const batchT = rawRowStructName(c.name, n.id);
2867
2477
  const portsT = `${portsStructName(c.name, n.id)}Batch`;
2868
- if (elemDeBox) {
2869
- // a record-element fanout returns the aligned element WIRES; a null aligned body is a DANGLING
2870
- // id → None (the covered runner maps it to the zero elem row).
2871
- methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<Option<ScriptedWireRow>>, BehaviorError> {
2478
+ methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<Option<ScriptedWireValue>>, BehaviorError> {
2872
2479
  let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)}).ok_or_else(|| unknown_component(${rustStrLit(f.component)}))?;
2873
2480
  if is_err {
2874
2481
  return Err(leaf_failure(err));
@@ -2878,201 +2485,91 @@ impl WireList for ScriptedWireList {
2878
2485
  .into_iter()
2879
2486
  .map(|ev| match ev {
2880
2487
  Value::Null => None,
2881
- _ => Some(ScriptedWireRow::new(ev)),
2488
+ _ => Some(ScriptedWireValue::new(ev)),
2882
2489
  })
2883
2490
  .collect();
2884
2491
  Ok(wires)
2885
- }`);
2886
- continue;
2887
- }
2888
- const elemRowRef = fanoutItemsElemRef(n, ref, plan);
2889
- const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2890
- methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<${batchT}, BehaviorError> {
2891
- let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)}).ok_or_else(|| unknown_component(${rustStrLit(f.component)}))?;
2892
- if is_err {
2893
- return Err(leaf_failure(err));
2894
- }
2895
- let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
2896
- let rows = arr
2897
- .iter()
2898
- .map(|ev| match ev {
2899
- Value::Null => ${elemT}::default(),
2900
- _ => ${elemDecode}(ev),
2901
- })
2902
- .collect();
2903
- Ok(${batchT} { rows })
2904
2492
  }`);
2905
2493
  continue;
2906
2494
  }
2907
2495
  if ("map" in n) {
2496
+ // map: batched → one WIRE per element (Vec<ScriptedWireValue>); non-batched → one element WIRE.
2908
2497
  const m = n.map;
2909
2498
  const batched = m.batched === true;
2910
- const arrRef = ref;
2911
- const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
2912
- const elemT = rawElemStructName(c.name, n.id);
2913
- const batchT = rawRowStructName(c.name, n.id);
2914
2499
  const portsT = batched ? `${portsStructName(c.name, n.id)}Batch` : portsStructName(c.name, n.id);
2915
- if (elemDeBox) {
2916
- // a record-element map returns the element WIRE(s); the covered runner de-boxes each strictly.
2917
- if (batched) {
2918
- methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<ScriptedWireRow>, BehaviorError> {
2919
- let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
2920
- if is_err {
2921
- return Err(leaf_failure(err));
2922
- }
2923
- let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
2924
- let wires = arr.into_iter().map(ScriptedWireRow::new).collect();
2925
- Ok(wires)
2926
- }`);
2927
- }
2928
- else {
2929
- methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<ScriptedWireRow, BehaviorError> {
2930
- let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
2931
- if is_err {
2932
- return Err(leaf_failure(err));
2933
- }
2934
- Ok(ScriptedWireRow::new(ok.unwrap_or(Value::Null)))
2935
- }`);
2936
- }
2937
- continue;
2938
- }
2939
- const elemRowRef = mapElemRowRef(n, arrRef, plan);
2940
- const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2941
2500
  if (batched) {
2942
- methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<${batchT}, BehaviorError> {
2501
+ methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<ScriptedWireValue>, BehaviorError> {
2943
2502
  let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
2944
2503
  if is_err {
2945
2504
  return Err(leaf_failure(err));
2946
2505
  }
2947
2506
  let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
2948
- let rows = arr.iter().map(${elemDecode}).collect();
2949
- Ok(${batchT} { rows })
2507
+ let wires = arr.into_iter().map(ScriptedWireValue::new).collect();
2508
+ Ok(wires)
2950
2509
  }`);
2951
2510
  }
2952
2511
  else {
2953
- methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<${elemT}, BehaviorError> {
2512
+ methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<ScriptedWireValue, BehaviorError> {
2954
2513
  let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
2955
2514
  if is_err {
2956
2515
  return Err(leaf_failure(err));
2957
2516
  }
2958
- let v = ok.unwrap_or(Value::Null);
2959
- Ok(${elemDecode}(&v))
2517
+ Ok(ScriptedWireValue::new(ok.unwrap_or(Value::Null)))
2960
2518
  }`);
2961
2519
  }
2962
2520
  continue;
2963
2521
  }
2522
+ // componentRef: return the whole result WIRE (one ScriptedWireValue wrapping the scripted ok Value);
2523
+ // the covered runner's INLINE de-box classifies it. A real consumer returns its own wire payload.
2964
2524
  const node = n;
2965
- const rowT = rawRowStructName(c.name, node.id);
2966
- // a de-box-eligible node's scripted handler returns a ScriptedWireRow (the scripted Value wrapped as
2967
- // a WireRow that classifies each attribute's native type into a DynamoDB-flavored wire tag); the
2968
- // covered module's generated decode_wire_* then probes it. A real consumer returns its wire payload
2969
- // for BC to de-box — the de-box no longer lives here.
2970
- if (deBoxEligible(ref)) {
2971
- // a record-containing top returns its wire (ScriptedWireRow / Vec / Option / BTreeMap) the
2972
- // covered runner de-boxes each record. #129: an arr node with an `items` port echoes the NATIVE
2973
- // port array, serialized back to Vec<ScriptedWireRow> so the de-box round-trips.
2974
- const top = recordTop(ref);
2975
- const wireT = (() => {
2976
- switch (top.kind) {
2977
- case "named":
2978
- return "ScriptedWireRow";
2979
- case "arr":
2980
- return "Vec<ScriptedWireRow>";
2981
- case "opt":
2982
- return "Option<ScriptedWireRow>";
2983
- case "map":
2984
- return "std::collections::BTreeMap<String, ScriptedWireRow>";
2525
+ // echo (#129/#141 fixtures): the scripted leaf can ECHO an input PORT back as its result wire, so the
2526
+ // input-port value flows into the de-boxed result and the runtime non-vacuity asserts survive.
2527
+ const echoBranches = [];
2528
+ if (ref.kind === "named") {
2529
+ const decl = findDecl(plan, ref.name);
2530
+ const field = decl !== undefined && decl.fields.length === 1 ? decl.fields[0] : undefined;
2531
+ const fieldRust = field !== undefined ? renderTypeRef(field.type) : null;
2532
+ const recordRust = renderTypeRef(ref);
2533
+ for (const pn of Object.keys(node.ports)) {
2534
+ let portRust = null;
2535
+ try {
2536
+ portRust = portFieldRustType(node.ports[pn], c, plan, typedNodes, `echo port '${pn}'`);
2985
2537
  }
2986
- })();
2987
- // echo-into-record (#141 fixtures): a named de-box node whose result WRAPS an input port — the
2988
- // scripted leaf echoes port P into the record's single field, so the input-port value flows into
2989
- // the de-boxed result and the runtime non-vacuity #139/#143/#144 assert survives the record wrap.
2990
- let namedEcho = "";
2991
- if (top.kind === "named") {
2992
- const decl = findDecl(plan, top.innerName);
2993
- const recordRust = renderTypeRef(ref);
2994
- const field = decl !== undefined && decl.fields.length === 1 ? decl.fields[0] : undefined;
2995
- const fieldRust = field !== undefined ? renderTypeRef(field.type) : null;
2996
- for (const pn of Object.keys(node.ports)) {
2997
- let portRust = null;
2998
- try {
2999
- portRust = portFieldRustType(node.ports[pn], c, plan, typedNodes, `echo port '${pn}'`);
3000
- }
3001
- catch {
3002
- portRust = null;
3003
- }
3004
- if (portRust === recordRust) {
3005
- // case 2: the port IS the whole record → echo it DIRECTLY (no field wrap), matching
3006
- // run_behavior's plain echo:port of a whole-record port.
3007
- const own = typeRefIsCopy(ref) ? `ports.${portFieldName(pn)}` : `ports.${portFieldName(pn)}.clone()`;
3008
- namedEcho += ` if echo == ${rustStrLit(pn)} {\n let tv = ${own};\n return Ok(ScriptedWireRow::new(${serializeTyped("tv", ref, plan)}));\n }\n`;
3009
- }
3010
- else if (fieldRust !== null && field !== undefined && portRust === fieldRust) {
3011
- // case 1: a scalar port wraps into the record's single field ({v: ports.X}).
3012
- const own = typeRefIsCopy(field.type) ? `ports.${portFieldName(pn)}` : `ports.${portFieldName(pn)}.clone()`;
3013
- namedEcho += ` if echo == ${rustStrLit(pn)} {\n let tv = ${own};\n return Ok(ScriptedWireRow::new(Value::Obj(vec![(${rustStrLit(field.name)}.to_string(), ${serializeTyped("tv", field.type, plan)})])));\n }\n`;
3014
- }
2538
+ catch {
2539
+ portRust = null;
2540
+ }
2541
+ if (portRust === recordRust) {
2542
+ // the port IS the whole record → echo it DIRECTLY (matching run_behavior's plain echo:port).
2543
+ const own = typeRefIsCopy(ref) ? `ports.${portFieldName(pn)}` : `ports.${portFieldName(pn)}.clone()`;
2544
+ echoBranches.push(` if echo == ${rustStrLit(pn)} {\n let tv = ${own};\n return Ok(ScriptedWireValue::new(${serializeTyped("tv", ref, plan)}));\n }`);
2545
+ }
2546
+ else if (fieldRust !== null && field !== undefined && portRust === fieldRust) {
2547
+ // a scalar port wraps into the record's single field ({v: ports.X}).
2548
+ const own = typeRefIsCopy(field.type) ? `ports.${portFieldName(pn)}` : `ports.${portFieldName(pn)}.clone()`;
2549
+ echoBranches.push(` if echo == ${rustStrLit(pn)} {\n let tv = ${own};\n return Ok(ScriptedWireValue::new(Value::Obj(vec![(${rustStrLit(field.name)}.to_string(), ${serializeTyped("tv", field.type, plan)})])));\n }`);
3015
2550
  }
3016
2551
  }
3017
- const canEcho = (top.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items")) || namedEcho !== "";
3018
- const portsParam = canEcho ? "ports" : "_ports";
3019
- const okBind = canEcho ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
3020
- let body;
3021
- if (top.kind === "named") {
3022
- body = `${namedEcho} Ok(ScriptedWireRow::new(ok.unwrap_or(Value::Null)))`;
3023
- }
3024
- else if (top.kind === "arr") {
3025
- const elemRef = ref.elem;
3026
- const itemsF = portFieldName("items");
3027
- const echoBranch = canEcho
3028
- ? ` if echo == "items" {\n let wires: Vec<ScriptedWireRow> = ports.${itemsF}.iter().map(|e| { let tv = e.clone(); ScriptedWireRow::new(${serializeTyped("tv", elemRef, plan)}) }).collect();\n return Ok(wires);\n }\n`
3029
- : "";
3030
- body = `${echoBranch} let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };\n Ok(arr.into_iter().map(ScriptedWireRow::new).collect())`;
3031
- }
3032
- else if (top.kind === "opt") {
3033
- body = ` match ok {\n None | Some(Value::Null) => Ok(None),\n Some(v) => Ok(Some(ScriptedWireRow::new(v))),\n }`;
3034
- }
3035
- else {
3036
- body = ` let obj = match ok { Some(Value::Obj(pairs)) => pairs, _ => Vec::new() };\n Ok(obj.into_iter().map(|(k, v)| (k, ScriptedWireRow::new(v))).collect())`;
3037
- }
3038
- methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Result<${wireT}, BehaviorError> {
3039
- let ${okBind} = self.next(${rustStrLit(node.component)}).ok_or_else(|| unknown_component(${rustStrLit(node.component)}))?;
3040
- if is_err {
3041
- return Err(leaf_failure(err));
3042
- }
3043
- ${body}
3044
- }`);
3045
- continue;
3046
2552
  }
3047
- const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
3048
- // #129 (TEST glue): if this node has an `items` port and its outType is a bare arr, support the
3049
- // reference scriptedHandlers' `echo === "items"` — the scripted handler ECHOES the NATIVE `f_items`
3050
- // port back as its result. This makes the node→leaf array dataflow test non-vacuous: the leaf's
3051
- // observed result is exactly the native array that flowed into its ports struct, so a dropped or
3052
- // boxed native array would diverge from run_behavior. `ports` is bound only on the echo path.
3053
- const canEcho = ref.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items");
2553
+ else if (ref.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items")) {
2554
+ // #129: an arr node with an `items` port echoes the NATIVE port array (serialized element-by-element
2555
+ // into a Value::Arr) so the node→leaf array dataflow test is non-vacuous.
2556
+ const elemRef = ref.elem;
2557
+ echoBranches.push(` if echo == "items" {\n let tv = Value::Arr(ports.${portFieldName("items")}.iter().map(|e| { let ev = e.clone(); ${serializeTyped("ev", elemRef, plan)} }).collect());\n return Ok(ScriptedWireValue::new(tv));\n }`);
2558
+ }
2559
+ const canEcho = echoBranches.length > 0;
3054
2560
  const portsParam = canEcho ? "ports" : "_ports";
3055
- const echoBranch = canEcho
3056
- ? `\n if echo == "items" {\n return Ok(${rowT} { val: ${portsParam}.${portFieldName("items")}.clone() });\n }`
3057
- : "";
3058
2561
  const okBind = canEcho ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
3059
- methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Result<${rowT}, BehaviorError> {
2562
+ const echoBlock = canEcho ? `${echoBranches.join("\n")}\n` : "";
2563
+ methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Result<ScriptedWireValue, BehaviorError> {
3060
2564
  let ${okBind} = self.next(${rustStrLit(node.component)}).ok_or_else(|| unknown_component(${rustStrLit(node.component)}))?;
3061
2565
  if is_err {
3062
2566
  return Err(leaf_failure(err));
3063
- }${echoBranch}
3064
- let v = ok.unwrap_or(Value::Null);
3065
- Ok(${decode}(&v))
2567
+ }
2568
+ ${echoBlock} Ok(ScriptedWireValue::new(ok.unwrap_or(Value::Null)))
3066
2569
  }`);
3067
2570
  }
3068
- const cHasDeBox = c.body.some((n) => {
3069
- if ("cond" in n)
3070
- return false;
3071
- if ("map" in n || "fanout" in n)
3072
- return mapFanoutElemDeBoxEligible(n, typedNodes, plan);
3073
- return deBoxEligible(typedNodes.get(n.id));
3074
- });
3075
- const wireAssoc = cHasDeBox ? ` type Wire = ScriptedWireRow;\n` : "";
2571
+ const cHasHandler = c.body.some((n) => !("cond" in n));
2572
+ const wireAssoc = cHasHandler ? ` type Wire = ScriptedWireValue;\n` : "";
3076
2573
  traitImpls.push(`impl ${handlerTraitName(c.name)} for ScriptedNativeRaw {\n${wireAssoc}${methods.join("\n")}\n}`);
3077
2574
  }
3078
2575
  const arms = native
@@ -3218,13 +2715,10 @@ ${marshallers}
3218
2715
 
3219
2716
  ${emitMarshalHelpers()}
3220
2717
 
3221
- // per-node concrete-row decoders (scripted Value -> RawRowNR_/RawElemNR_ struct).
3222
- ${decodeFns.join("\n\n")}
3223
-
3224
2718
  // input decoders (generic &[(String,Value)] -> concrete InNR_<comp>; TEST glue, one decode at the boundary).
3225
2719
  ${inDecoders}
3226
2720
 
3227
- // scripted wire adapter (TEST glue) — wraps a scripted Value as a WireRow the generated de-box probes.
2721
+ // scripted wire adapter (TEST glue) — wraps a scripted Value as a WireValue the generated inline de-box probes.
3228
2722
  ${wireAdapter}
3229
2723
 
3230
2724
  ${adapter}
@@ -3315,31 +2809,6 @@ function emitInDecoder(comp, plan) {
3315
2809
  lines.push(`}`);
3316
2810
  return lines.join("\n");
3317
2811
  }
3318
- /** emitDecodeRow — emit (once, deduped) a decoder `decode_row_<comp>_<node><suffix>(v: &Value) -> <rowT>`
3319
- * that materializes the scripted ok Value into the concrete row struct. Named: reuse the Value->struct
3320
- * marshaller + move fields; scalar/arr/opt: materialize into .val. TEST glue only. */
3321
- function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix) {
3322
- const fn = `decode_row_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}${suffix.toLowerCase()}`;
3323
- if (emitted.has(fn))
3324
- return fn;
3325
- emitted.add(fn);
3326
- const lines = [];
3327
- lines.push(`fn ${fn}(v: &Value) -> ${rowT} {`);
3328
- if (ref.kind === "named") {
3329
- const decl = findDecl(plan, ref.name);
3330
- lines.push(` let t = marshal_${ref.name}(v).expect("scripted row decode");`);
3331
- const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: t.${rustFieldName(f.name)}`).join(", ");
3332
- lines.push(` ${rowT} { ${inits} }`);
3333
- }
3334
- else {
3335
- const mat = materializeExpr("v", ref, plan);
3336
- lines.push(` let val = (|| -> Result<${renderTypeRef(ref)}, BehaviorError> { Ok(${mat}) })().expect("scripted row decode");`);
3337
- lines.push(` ${rowT} { val }`);
3338
- }
3339
- lines.push(`}`);
3340
- out.push(lines.join("\n"));
3341
- return fn;
3342
- }
3343
2812
  export const rustTypedNativeEmitter = {
3344
2813
  language: "rust-typed-native",
3345
2814
  classification: "codegen", // native: 型付きフラットコード生成・IR/dict 非在(#128/A6)