behavior-contracts 0.8.5 → 0.8.7

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 (41) hide show
  1. package/dist/authoring.d.ts.map +1 -1
  2. package/dist/authoring.js +25 -1
  3. package/dist/authoring.js.map +1 -1
  4. package/dist/behavior.d.ts +23 -3
  5. package/dist/behavior.d.ts.map +1 -1
  6. package/dist/behavior.js +209 -11
  7. package/dist/behavior.js.map +1 -1
  8. package/dist/builder.d.ts.map +1 -1
  9. package/dist/builder.js +12 -1
  10. package/dist/builder.js.map +1 -1
  11. package/dist/expr-eval.d.ts +7 -1
  12. package/dist/expr-eval.d.ts.map +1 -1
  13. package/dist/expr-eval.js +8 -1
  14. package/dist/expr-eval.js.map +1 -1
  15. package/dist/generator/core.d.ts +28 -0
  16. package/dist/generator/core.d.ts.map +1 -1
  17. package/dist/generator/core.js +1 -0
  18. package/dist/generator/core.js.map +1 -1
  19. package/dist/generator/emit-shared-debox.d.ts +108 -0
  20. package/dist/generator/emit-shared-debox.d.ts.map +1 -0
  21. package/dist/generator/emit-shared-debox.js +117 -0
  22. package/dist/generator/emit-shared-debox.js.map +1 -0
  23. package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
  24. package/dist/generator/emit-typed-native-go.js +923 -494
  25. package/dist/generator/emit-typed-native-go.js.map +1 -1
  26. package/dist/generator/emit-typed-native-rust.d.ts +9 -6
  27. package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
  28. package/dist/generator/emit-typed-native-rust.js +950 -505
  29. package/dist/generator/emit-typed-native-rust.js.map +1 -1
  30. package/dist/guard.d.ts.map +1 -1
  31. package/dist/guard.js +12 -0
  32. package/dist/guard.js.map +1 -1
  33. package/dist/index.d.ts +4 -4
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +4 -4
  36. package/dist/index.js.map +1 -1
  37. package/dist/plan.d.ts +43 -2
  38. package/dist/plan.d.ts.map +1 -1
  39. package/dist/plan.js +19 -5
  40. package/dist/plan.js.map +1 -1
  41. package/package.json +2 -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
@@ -20,25 +21,71 @@ function rustErr(code, msgExpr) {
20
21
  * fail closed with ZERO bc-runtime import. Codes match run_behavior verbatim (byte-equal). `code()` /
21
22
  * `failure_code()` expose the stable code without a bc-runtime type. */
22
23
  function emitBehaviorErrorType() {
23
- return `// BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
24
+ return `// ErrorKindwhat went wrong (the closed set of scp-error.md). A concrete enum: the covered plane
25
+ // carries no strings-as-tags and no dynamic kind lookup.
26
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
27
+ pub enum ErrorKind {
28
+ TypeMismatch,
29
+ MissingField,
30
+ Overflow,
31
+ }
32
+
33
+ impl ErrorKind {
34
+ pub fn as_str(self) -> &'static str {
35
+ match self {
36
+ ErrorKind::TypeMismatch => "typeMismatch",
37
+ ErrorKind::MissingField => "missingField",
38
+ ErrorKind::Overflow => "overflow",
39
+ }
40
+ }
41
+ }
42
+
43
+ // ErrorDetail — the structured, recoverable payload a failure carries (scp-error.md "The Error
44
+ // Value"). The LEAF produces it at the wire boundary (it is the only party holding both the declared
45
+ // type and the raw wire datum); the runner transports it verbatim. Concrete fields — no boxed Value,
46
+ // no serialized blob in a string: \`expected_type\` is Portable Type Notation, a rendering of a
47
+ // STATICALLY DECLARED type, so nothing walks a type at runtime.
48
+ #[derive(Debug, Clone, PartialEq, Eq, Default)]
49
+ pub struct ErrorDetail {
50
+ pub kind: Option<ErrorKind>,
51
+ pub model: Option<String>,
52
+ pub field: Option<String>,
53
+ pub expected_type: Option<String>,
54
+ pub actual_wire_type: Option<String>,
55
+ pub raw_value: Option<String>,
56
+ pub context: std::collections::BTreeMap<String, String>,
57
+ }
58
+
59
+ // BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
24
60
  // covered runner returns \`Result<T, BehaviorError>\` over THIS local type instead of a bc-runtime
25
61
  // failure, so the fully-covered module imports ZERO bc runtime. Codes match run_behavior verbatim
26
- // (byte-equal). The observe companion (test glue, a super:: submodule) bridges this local error to the
27
- // bc-runtime BehaviorError at the observe boundary the covered module itself stays runtime-free.
62
+ // (byte-equal). \`detail\` carries the leaf's structured Error Value across the seam so a caller can
63
+ // log / skip / repair / migrate rather than parse a message. The observe companion (test glue, a
64
+ // super:: submodule) bridges this local error to the bc-runtime failure at the observe boundary —
65
+ // the covered module itself stays runtime-free.
28
66
  #[derive(Debug, Clone)]
29
67
  pub struct BehaviorError {
30
68
  pub code: String,
31
69
  pub message: String,
70
+ pub detail: Option<Box<ErrorDetail>>,
32
71
  }
33
72
 
34
73
  impl BehaviorError {
35
74
  pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
36
- BehaviorError { code: code.into(), message: message.into() }
75
+ BehaviorError { code: code.into(), message: message.into(), detail: None }
76
+ }
77
+ /// The same failure carrying the leaf's structured Error Value.
78
+ pub fn with_detail(code: impl Into<String>, message: impl Into<String>, detail: ErrorDetail) -> Self {
79
+ BehaviorError { code: code.into(), message: message.into(), detail: Some(Box::new(detail)) }
37
80
  }
38
81
  /// The stable failure code (byte-equal to run_behavior) WITHOUT a bc-runtime type.
39
82
  pub fn code(&self) -> &str {
40
83
  &self.code
41
84
  }
85
+ /// The structured payload, if this failure is about a datum.
86
+ pub fn detail(&self) -> Option<&ErrorDetail> {
87
+ self.detail.as_deref()
88
+ }
42
89
  }
43
90
 
44
91
  impl std::fmt::Display for BehaviorError {
@@ -46,7 +93,75 @@ impl std::fmt::Display for BehaviorError {
46
93
  write!(f, "{}: {}", self.code, self.message)
47
94
  }
48
95
  }
49
- impl std::error::Error for BehaviorError {}`;
96
+ impl std::error::Error for BehaviorError {}
97
+
98
+ // op_failed — wrap a leaf failure as the node's Failure under its Error Policy Kind, carrying the
99
+ // leaf's structured detail through unchanged (the runner transports the Error Value; it does not
100
+ // synthesize or re-interpret it).
101
+ #[allow(dead_code)]
102
+ fn op_failed(node: &str, policy: &str, e: BehaviorError) -> BehaviorError {
103
+ BehaviorError {
104
+ code: "OP_FAILED".to_string(),
105
+ message: format!("operation '{node}' failed under '{policy}' policy: {}", e.message),
106
+ detail: e.detail,
107
+ }
108
+ }
109
+
110
+ // de_type_mismatch / de_missing_field / de_overflow — the de-box mismatch failures the generated decode
111
+ // produces (scp-error.md "The Error Value"). Codes are byte-equal to the runtime outType check
112
+ // (TYPE_MISMATCH / MISSING_PROP) so the covered read stays equivalent to run_behavior; detail.kind
113
+ // distinguishes typeMismatch / missingField / overflow.
114
+ #[allow(dead_code)]
115
+ fn de_type_mismatch(model: &str, field: &str, expected: &str, actual_wire: String, raw: String) -> BehaviorError {
116
+ BehaviorError::with_detail(
117
+ "TYPE_MISMATCH",
118
+ format!("node '{model}': {field}: expected {expected}, got {actual_wire}"),
119
+ ErrorDetail {
120
+ kind: Some(ErrorKind::TypeMismatch),
121
+ model: Some(model.to_string()),
122
+ field: Some(field.to_string()),
123
+ expected_type: Some(expected.to_string()),
124
+ actual_wire_type: Some(actual_wire),
125
+ raw_value: Some(raw),
126
+ context: std::collections::BTreeMap::new(),
127
+ },
128
+ )
129
+ }
130
+
131
+ #[allow(dead_code)]
132
+ fn de_missing_field(model: &str, field: &str, expected: &str) -> BehaviorError {
133
+ BehaviorError::with_detail(
134
+ "MISSING_PROP",
135
+ format!("node '{model}': {field}: required field is absent"),
136
+ ErrorDetail {
137
+ kind: Some(ErrorKind::MissingField),
138
+ model: Some(model.to_string()),
139
+ field: Some(field.to_string()),
140
+ expected_type: Some(expected.to_string()),
141
+ actual_wire_type: None,
142
+ raw_value: None,
143
+ context: std::collections::BTreeMap::new(),
144
+ },
145
+ )
146
+ }
147
+
148
+ #[allow(dead_code)]
149
+ fn de_overflow(model: &str, field: &str, expected: &str, actual_wire: String, raw: String) -> BehaviorError {
150
+ let message = format!("node '{model}': {field}: {raw} is outside the range of {expected}");
151
+ BehaviorError::with_detail(
152
+ "TYPE_MISMATCH",
153
+ message,
154
+ ErrorDetail {
155
+ kind: Some(ErrorKind::Overflow),
156
+ model: Some(model.to_string()),
157
+ field: Some(field.to_string()),
158
+ expected_type: Some(expected.to_string()),
159
+ actual_wire_type: Some(actual_wire),
160
+ raw_value: Some(raw),
161
+ context: std::collections::BTreeMap::new(),
162
+ },
163
+ )
164
+ }`;
50
165
  }
51
166
  function portFieldName(wire) {
52
167
  const safe = wire.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase();
@@ -58,14 +173,6 @@ function pascal(s) {
58
173
  function portsStructName(compName, nodeId) {
59
174
  return `PortsNR${pascal(compName)}${pascal(nodeId)}`;
60
175
  }
61
- /** RawRowNR<Comp><Node> — the concrete per-node handler-result row struct name. */
62
- function rawRowStructName(compName, nodeId) {
63
- return `RawRowNR${pascal(compName)}${pascal(nodeId)}`;
64
- }
65
- /** RawElemNR<Comp><Node> — the concrete per-ELEMENT row struct name for a covered map node. */
66
- function rawElemStructName(compName, nodeId) {
67
- return `RawElemNR${pascal(compName)}${pascal(nodeId)}`;
68
- }
69
176
  /** HandlerNR<Comp> — the per-component concrete handler trait name (one node_* method per node). */
70
177
  function handlerTraitName(compName) {
71
178
  return `HandlerNR${pascal(compName)}`;
@@ -78,6 +185,204 @@ function nodeMethodName(nodeId) {
78
185
  function inStructName(compName) {
79
186
  return `InNR${pascal(compName)}`;
80
187
  }
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). */
199
+ function emitRustProbeWireTypes() {
200
+ return `// Probe<T> / NumProbe — the outcome of classifying one wire attribute against a declared type. Got carries
201
+ // the matched value (a NumProbe's raw numeric text, which the de-box parses + range-checks so overflow is
202
+ // BC's to detect); actual_wire_type is the producer's own wire tag (a free string, e.g. a DynamoDB
203
+ // "S"/"N"/"BOOL"); raw_value is the offending value stringified. Concrete enums — no boxed Value.
204
+ pub enum Probe<T> {
205
+ Got(T),
206
+ Wrong { actual_wire_type: String, raw_value: String },
207
+ Null { actual_wire_type: String, raw_value: String },
208
+ Absent,
209
+ }
210
+
211
+ pub enum NumProbe {
212
+ Got { raw: String, actual_wire_type: String },
213
+ Wrong { actual_wire_type: String, raw_value: String },
214
+ Null { actual_wire_type: String, raw_value: String },
215
+ Absent,
216
+ }
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
+
232
+ // WireRow / WireList — the consumer's wire item / wire array, probed per declared field / element by the
233
+ // generated de-box. The consumer implements them over its wire payload (a DynamoDB AttributeValue map)
234
+ // and classifies each attribute's variant; the de-box owns strictness. Monomorphized (Row/List mutual
235
+ // associated types) — no dyn, no Box, no boxed Value crosses the seam.
236
+ pub trait WireRow: Sized {
237
+ type List: WireList<Row = Self>;
238
+ // the attribute keys present (the entries of a wire map / DynamoDB "M") — iterated to decode a map(V).
239
+ fn keys(&self) -> Vec<String>;
240
+ fn probe_string(&self, field: &str) -> Probe<String>;
241
+ fn probe_number(&self, field: &str) -> NumProbe;
242
+ fn probe_bool(&self, field: &str) -> Probe<bool>;
243
+ fn probe_row(&self, field: &str) -> Probe<Self>;
244
+ fn probe_list(&self, field: &str) -> Probe<Self::List>;
245
+ }
246
+
247
+ pub trait WireList: Sized {
248
+ type Row: WireRow<List = Self>;
249
+ fn len(&self) -> usize;
250
+ fn is_empty(&self) -> bool {
251
+ self.len() == 0
252
+ }
253
+ fn elem_string(&self, i: usize) -> Probe<String>;
254
+ fn elem_number(&self, i: usize) -> NumProbe;
255
+ fn elem_bool(&self, i: usize) -> Probe<bool>;
256
+ fn elem_row(&self, i: usize) -> Probe<Self::Row>;
257
+ fn elem_list(&self, i: usize) -> Probe<Self>;
258
+ }`;
259
+ }
260
+ /**
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.
274
+ */
275
+ function renderRustDeBox(plan, srcExpr, ctr, indent, fail, idxExpr = "") {
276
+ const I = indent;
277
+ const I1 = indent + " ";
278
+ const id = ctr.n++;
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})`;
296
+ }
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")} {
317
+ ${I1}NumProbe::Got { raw, actual_wire_type } => match raw.parse::<${rustT}>() {
318
+ ${I1} Ok(n) => ${wrapSome("n")},
319
+ ${I1} Err(_) => ${fail(`de_overflow(${ml}, ${fl}, ${nl}, actual_wire_type, raw)`)},
320
+ ${I1}},
321
+ ${numArms}
322
+ ${I}}`;
323
+ }
324
+ const meth = plan.scalar === "string" ? "string" : "bool";
325
+ return `match ${probe(meth)} {
326
+ ${I1}Probe::Got(v) => ${wrapSome("v")},
327
+ ${probeArms}
328
+ ${I}}`;
329
+ }
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}
339
+ ${I}}`;
340
+ }
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());
346
+ ${I1} for i${id} in 0..l${id}.len() {
347
+ ${I1} acc${id}.push(${elemExpr});
348
+ ${I1} }
349
+ ${I1} acc${id}
350
+ ${I1}}`)},
351
+ ${probeArms}
352
+ ${I}}`;
353
+ }
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();
359
+ ${I1} for k${id} in m${id}.keys() {
360
+ ${I1} let v${id} = ${valExpr};
361
+ ${I1} acc${id}.insert(k${id}, v${id});
362
+ ${I1} }
363
+ ${I1} acc${id}
364
+ ${I1}}`)},
365
+ ${probeArms}
366
+ ${I}}`;
367
+ }
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);
378
+ }
379
+ /** mapFanoutElemRef — the element ROW TypeRef a map/fanout node decodes (the per-element record). */
380
+ function mapFanoutElemRef(node, typedNodes, plan) {
381
+ const ref = typedNodes.get(node.id);
382
+ if ("fanout" in node)
383
+ return fanoutItemsElemRef(node, ref, plan);
384
+ return mapElemRowRef(node, ref, plan);
385
+ }
81
386
  function staticArrElems(node) {
82
387
  if (node === null || typeof node !== "object" || Array.isArray(node))
83
388
  return null;
@@ -88,11 +393,60 @@ function staticArrElems(node) {
88
393
  return Array.isArray(arg) ? arg : null;
89
394
  }
90
395
  /**
91
- * portFieldRustType — the NATIVE Rust type for a covered port field (bc#90 / runtime-free), read off the
92
- * ONE port lowering (compilePortNode / native-expr). The covered ports struct carries NO `Value` field:
93
- * every port lowers to a concrete Rust type, and a port that cannot lower FAILS CLOSED. The field type is
94
- * the compiled expression's TypeRef, so it can never disagree with the initializer (emitPortInit), which
95
- * is the SAME lowering read for its value. `where` names the emission site for the error.
396
+ * portFieldRustType — the NATIVE Rust type for a covered port field (bc#90 / runtime-free). The covered
397
+ * ports struct carries NO `Value` field: every port lowers to a concrete Rust type.
398
+ /** rustOwn the ownership renderer native-expr applies at a resolved ref LEAF: an array field is cloned
399
+ * (owned) when `ownArrays` (a port) or borrowed (a cond `len`); a Copy scalar behind a guard is deref'd
400
+ * (unless a field was already accessed by value); anything else clones. */
401
+ function rustOwn(guard, ownArrays) {
402
+ return (expr, ref, fieldAccessed) => {
403
+ if (ref.kind === "arr")
404
+ return ownArrays ? `${expr}.clone()` : expr;
405
+ if (typeRefIsCopy(ref))
406
+ return guard && !fieldAccessed ? `*${expr}` : expr;
407
+ return `${expr}.clone()`;
408
+ };
409
+ }
410
+ /** nativeResolveHead — the ONE ref-head resolver the runtime-free native-expr compiler reads (cond `if`/
411
+ * branches, a map/fanout `when` guard, AND a port). A head is a `$as` element binding, a prior body node's
412
+ * typed cell (behind a RefCell borrow guard), or an input port (a by-value struct field). #139: the input
413
+ * ref comes from the inputPortTypeRef SSoT so an OPTIONAL port resolves as `{opt:…}` (native Option<T>). */
414
+ function nativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
415
+ const ownArrays = opts?.ownArrays === true;
416
+ return (head) => {
417
+ if (opts?.asBinding !== undefined && opts.asBase !== undefined && head === opts.asBinding.name) {
418
+ return { expr: opts.asBase, ref: opts.asBinding.ref, own: rustOwn(true, ownArrays) };
419
+ }
420
+ if (isPrior(head)) {
421
+ const ref = typedNodes.get(head);
422
+ return { expr: `${typedCell(head)}.borrow()`, ref, own: rustOwn(true, ownArrays) };
423
+ }
424
+ const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
425
+ if (ref === undefined)
426
+ return null;
427
+ return { expr: `in_.${rustFieldName(head)}`, ref, own: rustOwn(false, ownArrays) };
428
+ };
429
+ }
430
+ /** compilePortNode — lower a port expression to its native ports-struct field via the runtime-free
431
+ * native-expr compiler: the ONE port lowering. The port's field TYPE is the compiled `ref`, its
432
+ * initializer the compiled `expr` — derived from a SINGLE lowering, so they cannot disagree. A port that
433
+ * cannot lower FAILS CLOSED; a FALLIBLE lowering (hoists statements) is returned as-is (only emitPortInit
434
+ * rejects it). Reuses the same nativeResolveHead cond/guard/port share, with `ownArrays` (a port owns). */
435
+ function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
436
+ const resolveHead = nativeResolveHead(comp, plan, typedNodes, isPrior, { ...opts, ownArrays: true });
437
+ try {
438
+ return new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compilePort(node);
439
+ }
440
+ catch (e) {
441
+ if (!(e instanceof GeneratorFailure))
442
+ throw e;
443
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} ${JSON.stringify(node)} does not lower to a native Rust value: ${e.message}`);
444
+ }
445
+ }
446
+ /**
447
+ * portFieldRustType — the NATIVE Rust type for a covered port field, read off the ONE port lowering
448
+ * (compilePortNode / native-expr). Field type = the compiled TypeRef, so it can never disagree with the
449
+ * initializer (emitPortInit), which reads the SAME lowering for its value. `where` names the site.
96
450
  */
97
451
  function portFieldRustType(node, comp, plan, typedNodes, where = "port", opts) {
98
452
  const c = compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), where, opts);
@@ -105,12 +459,17 @@ function portFieldRustType(node, comp, plan, typedNodes, where = "port", opts) {
105
459
  * reads `ports.f_<field>` DIRECTLY. The struct derives Clone (a bc#87 parallel stage clones it into the
106
460
  * per-worker dispatch). Every field is a NATIVE Rust type (bc#90 / runtime-free) — NO boxed `Value`.
107
461
  */
108
- function emitPortsStruct(comp, node, plan, typedNodes, asBinding) {
462
+ function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
109
463
  const name = portsStructName(comp.name, node.id);
110
464
  const portNames = Object.keys(node.ports);
111
465
  if (portNames.length === 0) {
112
466
  return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
113
467
  }
468
+ // a node WITH ports always resolves through the one port lowering, which needs the type plan + the
469
+ // prior-node map (both are supplied by every caller — narrow the optional params for the fields below).
470
+ if (plan === undefined || typedNodes === undefined) {
471
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native: ports struct for node '${node.id}' needs a type plan + prior-node map`);
472
+ }
114
473
  const types = portNames.map((p) => portFieldRustType(node.ports[p], comp, plan, typedNodes, `port '${p}' of node '${node.id}'`, asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined));
115
474
  const fields = portNames
116
475
  .map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
@@ -135,7 +494,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
135
494
  // #108: element ports may read the `$as` binding — resolve the over element type for native typing.
136
495
  const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
137
496
  const asBinding = { name: m.as, ref: elemRef, plan };
138
- const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, plan, typedNodes, asBinding);
497
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan, typedNodes);
139
498
  return `${elemStruct}
140
499
 
141
500
  // ${batch} — CONCRETE batched ports for map '${node.id}': the Vec of per-element CONCRETE ports structs
@@ -153,7 +512,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
153
512
  const batch = `${name}Batch`;
154
513
  const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
155
514
  const asBinding = { name: f.as, ref: elemRef, plan };
156
- const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, plan, typedNodes, asBinding);
515
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan, typedNodes);
157
516
  return `${elemStruct}
158
517
 
159
518
  // ${batch} — CONCRETE batched ports for fanout '${node.id}': the Vec of per-id CONCRETE ports structs.
@@ -173,6 +532,20 @@ let rustExprTempSeq = 0;
173
532
  /** module-level accumulator: set when the native-expr compiler emits a fallible helper call (so emit()
174
533
  * bakes the helper library). Reset at the start of each emit(). */
175
534
  const rustExprUsed = { helpers: false };
535
+ /** rustBlockBody — the body of a block-expression that runs `stmts` then yields `expr`. A trailing
536
+ * `let t = E; t` collapses to `E` (clippy `let_and_return`). */
537
+ function rustBlockBody(stmts, expr) {
538
+ const last = stmts[stmts.length - 1];
539
+ const m = last !== undefined ? /^let\s+([A-Za-z0-9_]+)(?::\s*[^=]+)?\s*=\s*(.+);$/.exec(last) : null;
540
+ if (m !== null && m[1] === expr)
541
+ return [...stmts.slice(0, -1), m[2]].join(" ");
542
+ return [...stmts, rustStripOuterParens(expr)].join(" ");
543
+ }
544
+ /** Drop a TRAILING `.clone()` — for a position that only reads the value (a presence test), so an owning
545
+ * clone added by a head resolver is pure waste. Only a clone at the very END is dropped. */
546
+ function rustStripTrailingClone(expr) {
547
+ return expr.endsWith(".clone()") ? expr.slice(0, -".clone()".length) : expr;
548
+ }
176
549
  /** makeRustExprBackend — renders native-expr for Rust. `resolveHead` maps a ref head to an OWNED typed
177
550
  * base expr (a prior-node cell clone / an input field / a $as element). Sets rustExprUsed.helpers when
178
551
  * a fallible helper is emitted. The runner is Result<_, BehaviorError>, so hoistFallible uses `?`. */
@@ -216,47 +589,39 @@ function makeRustExprBackend(resolveHead, plan) {
216
589
  notOp: (a) => `(!${a})`,
217
590
  structLit: (name, inits) => `${name} { ${inits.map((i) => `${rustFieldName(i.field)}: ${i.expr}`).join(", ")} }`,
218
591
  arrLit: (elemTy, items) => `{ let __v: Vec<${elemTy}> = vec![${items.join(", ")}]; __v }`,
219
- // a projection port of static string literals — bare `&'static str` literals, ZERO heap allocation
220
- // (the field is `Vec<&'static str>`, so `vec!["a", "b"]` infers `&'static str` elements).
592
+ // a projection port of static string literals — `vec!["a", "b"]` infers `&'static str` (the field is
593
+ // Vec<&'static str>), ZERO heap allocation.
221
594
  staticStrArr: (literals) => ({ expr: `vec![${literals.map((s) => rustStrLit(s)).join(", ")}]`, type: "Vec<&'static str>" }),
222
- // an opt (`Option<T>`) NONE literal. Annotate as `Option::<T>::None` so it type-infers in ANY
223
- // position (a bare `None` can be ambiguous; the explicit turbofish keeps it a fully-typed value).
224
- optNone: (innerTy) => `Option::<${innerTy}>::None`,
225
- // opt (`Option<T>`) null-presence test: present = `expr.is_some()`, absent = `expr.is_none()`.
226
- // A presence test only READS the discriminant, so an owning clone of the operand (which a head
227
- // resolver adds so the value can be moved into a port) is dropped here — testing `Option<String>`
228
- // must not heap-allocate. Mirrors the go twin's zero-cost `!= nil`.
229
- optIsSome: (expr) => `${rustStripTrailingClone(expr)}.is_some()`,
230
- optIsNone: (expr) => `${rustStripTrailingClone(expr)}.is_none()`,
231
- // opt (`Option<T>`) defaulted to a PURE default — it cannot fail and has no effect, so its
232
- // evaluation order is UNOBSERVABLE and both forms below are correct (a FALLIBLE default, where
233
- // laziness IS observable, goes to optCoalesceGuard instead). `unwrap_or` is EAGER — fine for a plain
234
- // literal/copy. A default that allocates (an owned `String`, a struct/vec literal) uses
235
- // `unwrap_or_else`, whose closure defers the allocation to the absent branch: not a semantic
236
- // requirement here, but pointless work otherwise — and what clippy `or_fun_call` asks for.
595
+ // opt (Option<T>) defaulted to a PURE default eager unwrap_or for a plain copy/path, else the lazy
596
+ // unwrap_or_else closure (defers an allocating default to the absent branch; clippy or_fun_call).
237
597
  optUnwrapOr: (optExpr, defaultExpr, _innerTy) => /^[A-Za-z0-9_.:]+$/.test(defaultExpr) ? `${optExpr}.unwrap_or(${defaultExpr})` : `${optExpr}.unwrap_or_else(|| ${defaultExpr})`,
238
598
  // opt defaulted to a FALLIBLE default — a `match`, NOT a closure: the default's hoisted `?` early
239
- // returns must propagate out of the ENCLOSING fn, which `unwrap_or_else`'s closure would swallow
240
- // (it would try to return from the closure). Only the None arm evaluates the default.
599
+ // returns must propagate out of the ENCLOSING fn. Only the None arm evaluates the default.
241
600
  optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
242
601
  `let ${temp}: ${ty} = match ${optExpr} {`,
243
602
  ` Some(${bindTemp}) => ${bindTemp},`,
244
603
  ` None => { ${rustBlockBody(dStmts, dExpr)} }`,
245
604
  `};`,
246
605
  ],
606
+ // an opt (`Option<T>`) NONE literal. Annotate as `Option::<T>::None` so it type-infers in ANY
607
+ // position (a bare `None` can be ambiguous; the explicit turbofish keeps it a fully-typed value).
608
+ optNone: (innerTy) => `Option::<${innerTy}>::None`,
609
+ // opt (`Option<T>`) null-presence test: present = `expr.is_some()`, absent = `expr.is_none()`.
610
+ optIsSome: (expr) => `${rustStripTrailingClone(expr)}.is_some()`,
611
+ optIsNone: (expr) => `${rustStripTrailingClone(expr)}.is_none()`,
247
612
  ternary: (cond, t, e, _ty) => `(if ${rustStripOuterParens(cond)} { ${rustStripOuterParens(t)} } else { ${rustStripOuterParens(e)} })`,
248
613
  shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
249
614
  // and: let temp = if left { <rStmts> rExpr } else { false }; or: if left { true } else { <rStmts> rExpr }.
250
615
  // build a block that runs rStmts then yields rExpr (parens stripped — block-tail is not a sub-expr).
251
- const rblock = `{ ${rustBlockBody(rStmts, rExpr)} }`;
616
+ const rblock = `{ ${rStmts.join(" ")} ${rustStripOuterParens(rExpr)} }`;
252
617
  if (op === "and") {
253
618
  return [`let ${temp}: bool = if ${rustStripOuterParens(left)} ${rblock} else { false };`];
254
619
  }
255
620
  return [`let ${temp}: bool = if ${rustStripOuterParens(left)} { true } else ${rblock};`];
256
621
  },
257
622
  condGuard: (temp, ty, cond, tStmts, tExpr, eStmts, eExpr) => {
258
- const tblock = `{ ${rustBlockBody(tStmts, tExpr)} }`;
259
- const eblock = `{ ${rustBlockBody(eStmts, eExpr)} }`;
623
+ const tblock = `{ ${tStmts.join(" ")} ${rustStripOuterParens(tExpr)} }`;
624
+ const eblock = `{ ${eStmts.join(" ")} ${rustStripOuterParens(eExpr)} }`;
260
625
  return [`let ${temp}: ${ty} = if ${rustStripOuterParens(cond)} ${tblock} else ${eblock};`];
261
626
  },
262
627
  };
@@ -265,17 +630,6 @@ function makeRustExprBackend(resolveHead, plan) {
265
630
  function rustSnake(name) {
266
631
  return name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").replace(/([0-9])([a-z])/g, "$1_$2").toLowerCase();
267
632
  }
268
- /** rustBlockBody — the body of a block-expression that runs `stmts` then yields `expr`. A trailing
269
- * `let t = E; t` is collapsed to `E`: binding a temp only to return it on the next line is clippy's
270
- * `let_and_return` (an error under `-D warnings`), and the hoisted-fallible shape produces exactly that
271
- * whenever the block's tail IS the hoisted temp. */
272
- function rustBlockBody(stmts, expr) {
273
- const last = stmts[stmts.length - 1];
274
- const m = last !== undefined ? /^let\s+([A-Za-z0-9_]+)(?::\s*[^=]+)?\s*=\s*(.+);$/.exec(last) : null;
275
- if (m !== null && m[1] === expr)
276
- return [...stmts.slice(0, -1), m[2]].join(" ");
277
- return [...stmts, rustStripOuterParens(expr)].join(" ");
278
- }
279
633
  /** strip ONE redundant outer paren pair from an expr (clippy `if (cond)` unused_parens). Only when the
280
634
  * whole string is a single balanced `( … )` — a `(a) && (b)` stays untouched. */
281
635
  function rustStripOuterParens(expr) {
@@ -293,12 +647,6 @@ function rustStripOuterParens(expr) {
293
647
  }
294
648
  return expr.slice(1, -1);
295
649
  }
296
- /** Drop a TRAILING `.clone()` from an expression — for a position that only reads the value (no move),
297
- * so an owning clone added by a head resolver is pure waste. Only a clone at the very END is dropped: a
298
- * `.clone()` mid-path (`t_x.borrow().clone().field`) still owns the field access that follows it. */
299
- function rustStripTrailingClone(expr) {
300
- return expr.endsWith(".clone()") ? expr.slice(0, -".clone()".length) : expr;
301
- }
302
650
  /** Rust float literal (finite; integral gets `.0`). */
303
651
  function rustFloatLitExpr(n) {
304
652
  if (Number.isInteger(n))
@@ -389,73 +737,6 @@ fn bc_expr_str_gt(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) == std::cmp::
389
737
  #[allow(dead_code)]
390
738
  fn bc_expr_str_ge(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) != std::cmp::Ordering::Less }`;
391
739
  }
392
- // ── CONCRETE per-node row structs + row→outType copiers (mirror the Go concrete contract) ──────────
393
- //
394
- // Each covered node has a CONCRETE row struct `RawRowNR<Comp><Node>` whose fields are the node's projected
395
- // outType fields (typed, flattened) plus a per-node error signal (is_error/err). A named-struct outType
396
- // flattens to one Rust field per outType field; a scalar/arr/opt node carries a single `val: <typed>`
397
- // payload. The per-component `HandlerNR<Comp>` trait has one method per node returning that concrete
398
- // struct; the runner reads `row.<field>` DIRECTLY (no `RawValue`, no `match`, no `Value::`) and copies
399
- // each field into the node's outType struct local.
400
- /** emitRawRowStructs — emit the concrete `RawRowNR<Comp><Node>` struct per covered node (+ per-element
401
- * `RawElemNR<Comp><Node>` for a covered map). Every row carries the per-node error signal (is_error/err). */
402
- function emitRawRowStructs(comp, typedNodes, plan) {
403
- const parts = [];
404
- for (const n of comp.body) {
405
- if ("cond" in n)
406
- continue; // #108: a cond node has no handler-result row (pure Expression).
407
- const ref = typedNodes.get(n.id);
408
- if ("fanout" in n) {
409
- const elemRowRef = fanoutItemsElemRef(n, ref, plan);
410
- parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
411
- parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
412
- `#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub is_error: bool,\n pub err: String,\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
413
- continue;
414
- }
415
- if ("map" in n) {
416
- const arrRef = ref;
417
- const elemRowRef = mapElemRowRef(n, arrRef, plan);
418
- parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
419
- parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
420
- `#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub is_error: bool,\n pub err: String,\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
421
- continue;
422
- }
423
- parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
424
- }
425
- return parts.join("\n\n");
426
- }
427
- /** Emit one concrete row struct: flatten a named outType to typed fields, else a single `val` payload;
428
- * always carry is_error/err. Derives Default so a handler can build an error row cheaply (`..Default`). */
429
- function emitOneRowStruct(name, ref, plan) {
430
- const lines = [];
431
- lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType; + error signal).`);
432
- lines.push(`#[derive(Default)]`);
433
- lines.push(`pub struct ${name} {`);
434
- lines.push(` pub is_error: bool,`);
435
- lines.push(` pub err: String,`);
436
- if (ref.kind === "named") {
437
- const decl = findDecl(plan, ref.name);
438
- for (const f of decl.fields) {
439
- lines.push(` pub ${rustFieldName(f.name)}: ${renderTypeRef(f.type)}, // ${JSON.stringify(f.name)}`);
440
- }
441
- }
442
- else {
443
- lines.push(` pub val: ${renderTypeRef(ref)},`);
444
- }
445
- lines.push(`}`);
446
- return lines.join("\n");
447
- }
448
- /** emitRowCopy — the statement copying a concrete row's typed fields into the node's outType local
449
- * `target` (a value of Rust type renderTypeRef(ref)). Named: struct literal from the row's moved fields;
450
- * scalar/arr/opt: `.val`. `rowExpr` is the concrete row VALUE (moved). No dynamic read — pure move/copy. */
451
- function emitRowMove(target, rowExpr, ref, plan) {
452
- if (ref.kind === "named") {
453
- const decl = findDecl(plan, ref.name);
454
- const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: ${rowExpr}.${rustFieldName(f.name)}`).join(", ");
455
- return `${target} = ${ref.name} { ${inits} };`;
456
- }
457
- return `${target} = ${rowExpr}.val;`;
458
- }
459
740
  /**
460
741
  * boundRustType (change #3 — typed `bound`) — the CONCRETE Rust type of a covered node's `bound` handler
461
742
  * param (bc#90 / runtime-free). `bound` is the parent-produced key (the bindField value); it replaces the
@@ -503,10 +784,19 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
503
784
  const fnKwFor = (nodeId) => (ap.nodeIsAsync(nodeId) ? "async fn" : "fn");
504
785
  const lines = [];
505
786
  lines.push(`// ${handlerTraitName(comp.name)} — the CONCRETE per-component handler seam: one typed method`);
506
- lines.push(`// per covered node (native ports struct IN, concrete row struct OUT). No generic boxed-ports`);
507
- lines.push(`// / boxed-value / dynamic accessor crosses the covered boundary — the consumer implements each`);
508
- 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).`);
509
791
  lines.push(`pub trait ${handlerTraitName(comp.name)} {`);
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;`);
510
800
  for (const n of comp.body) {
511
801
  if ("cond" in n)
512
802
  continue; // #108: a cond node has no handler method (pure Expression).
@@ -514,21 +804,23 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
514
804
  const fnKw = fnKwFor(n.id);
515
805
  const bt = boundRustType(n, typedNodes, plan);
516
806
  if ("fanout" in n) {
517
- // fanout (v3): one batched dispatch of the deduped id list batch ports IN, aligned batch row OUT.
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).
518
810
  const portsT = `${portsStructName(comp.name, n.id)}Batch`;
519
- const retT = rawRowStructName(comp.name, n.id);
520
- lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Option<${retT}>;`);
811
+ lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Result<Vec<Option<Self::Wire>>, BehaviorError>;`);
521
812
  continue;
522
813
  }
523
814
  if ("map" in n) {
524
815
  const m = n.map;
525
816
  const batched = m.batched === true;
526
817
  const portsT = batched ? `${portsStructName(comp.name, n.id)}Batch` : portsStructName(comp.name, n.id);
527
- const retT = batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
528
- lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Option<${retT}>;`);
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";
820
+ lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Result<${retT}, BehaviorError>;`);
529
821
  continue;
530
822
  }
531
- lines.push(` ${fnKw} ${method}(&self, ports: &${portsStructName(comp.name, n.id)}, bound: ${bt}) -> Option<${rawRowStructName(comp.name, n.id)}>;`);
823
+ lines.push(` ${fnKw} ${method}(&self, ports: &${portsStructName(comp.name, n.id)}, bound: ${bt}) -> Result<Self::Wire, BehaviorError>;`);
532
824
  }
533
825
  lines.push(`}`);
534
826
  return lines.join("\n");
@@ -541,18 +833,16 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
541
833
  function mapElemMaterializerName(compName, nodeId) {
542
834
  return `materialize_map_elem_${sanitize(compName)}_${typedCell(nodeId)}`;
543
835
  }
544
- /** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType`, normalizing the two IR
545
- * conventions to the one array shape every downstream site assumes (`typedNodes.get(mapId)` is an arr):
546
- * - the RECORDER SoT (authoring.ts recordMap): `node.outType` is the ELEMENT (pre-array-wrap) wrap
547
- * it to `{arr:element}` here (the map produces `[]element`), matching type-gate's outputType wrap
548
- * (`"map" in n ? {arr:ot} : ot`). This is the declarative-authoring form (graphddb batchWrite/forEach).
549
- * - legacy hand-built IR: `node.outType` is already the `{arr:element}` produced form take it as-is.
550
- * A map ELEMENT is never itself an `{arr:…}` in the covered corpus, so the `arr`-keyed discriminator is
551
- * unambiguous. Both converge to the SAME produced-array ref, so existing goldens stay byte-identical. */
836
+ /** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType`.
837
+ *
838
+ * A map node's `outType` is its ELEMENT type (scp-error.md "Map element type"), so the produced array
839
+ * is `{arr: element}`. This is the same reading the compiler applies when it resolves what a node
840
+ * result reference has (`type-gate`: `"map" in n ? {arr:ot} : ot`) the emitter follows that SSoT
841
+ * rather than inspecting the annotation's shape, so an element that is ITSELF an `arr` stays
842
+ * expressible (element `arr(T)` produces `arr(arr(T))`). */
552
843
  function mapNodeArrRef(node, plan) {
553
844
  const outT = node.outType;
554
- const isArr = outT !== null && typeof outT === "object" && "arr" in outT;
555
- return deriveTypeRef((isArr ? outT : { arr: outT }), plan);
845
+ return deriveTypeRef({ arr: outT }, plan);
556
846
  }
557
847
  /** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
558
848
  * type (into) or the element outType directly (no-into). This is the type of RawElemNR<Comp><Node>. */
@@ -572,9 +862,8 @@ function mapElemRowRef(node, arrRef, plan) {
572
862
  }
573
863
  /**
574
864
  * emitMapElemMaterializers — for each covered map...into node, emit the AUGMENTED-element copier: copy
575
- * the over element's typed fields + move the `into` field from the concrete per-element handler row
576
- * (RawElemNR). Pure struct field assignment — NO dynamic value read (the handler already produced the
577
- * 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).
578
867
  */
579
868
  function emitMapElemMaterializers(comp, typedNodes, plan) {
580
869
  const parts = [];
@@ -596,7 +885,6 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
596
885
  const overFields = decl.fields.filter((f) => f.name !== intoKey);
597
886
  const overElemTy = mapOverElemType(comp, n, typedNodes, plan);
598
887
  const fn = mapElemMaterializerName(comp.name, n.id);
599
- const elemRowGo = rawElemStructName(comp.name, n.id);
600
888
  const intoRowRef = mapElemRowRef(n, arrRef, plan);
601
889
  const inits = [];
602
890
  // Copy scalars (i64/f64/bool) copy directly; String/named/arr/opt clone (clippy clone_on_copy clean).
@@ -604,15 +892,12 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
604
892
  const isCopy = f.type.kind === "scalar" && (f.type.scalar === "int" || f.type.scalar === "float" || f.type.scalar === "bool");
605
893
  inits.push(` ${rustFieldName(f.name)}: over.${rustFieldName(f.name)}${isCopy ? "" : ".clone()"},`);
606
894
  }
607
- // move the into field off the concrete element row (named struct literal; scalar/arr/opt .val).
608
- const intoValue = intoRowRef.kind === "named"
609
- ? `${intoRowRef.name} { ${findDecl(plan, intoRowRef.name).fields.map((f) => `${rustFieldName(f.name)}: into.${rustFieldName(f.name)}`).join(", ")} }`
610
- : `into.val`;
895
+ // the `into` param is the already-de-boxed into value (its native type) drop it straight in.
611
896
  parts.push(`#[allow(dead_code)]
612
- fn ${fn}(over: &${overElemTy}, into: ${elemRowGo}) -> ${elemRef.name} {
897
+ fn ${fn}(over: &${overElemTy}, into: ${renderTypeRef(intoRowRef)}) -> ${elemRef.name} {
613
898
  ${elemRef.name} {
614
899
  ${inits.join("\n")}
615
- ${rustFieldName(intoKey)}: ${intoValue},
900
+ ${rustFieldName(intoKey)}: into,
616
901
  }
617
902
  }`);
618
903
  void intoField;
@@ -664,26 +949,42 @@ function reconstructR(e) {
664
949
  return e.node;
665
950
  }
666
951
  }
667
- /** Does a port lower to a native value? The ELIGIBILITY gate ASKS the one port lowering (compilePortNode
668
- * / native-expr) whether it can lower this port, rather than re-deriving the shape rules so the gate
669
- * and the emitter can never disagree. A port that does not lower (or lowers fallibly) fails closed. */
670
- function portIsStatic(node, comp, plan, typedNodes, asBinding) {
671
- try {
672
- compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), "port", asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined);
673
- return true;
674
- }
675
- catch {
676
- return false; // an eligibility probe: any lowering failure not native-covered here.
677
- }
678
- }
679
- /** the prior-node typed-cell TypeRef map for a component (control-gate nodes are typeless; a map node
680
- * carries its produced-array ref). Shared by coverage, the struct surface, and the runner. */
952
+ /**
953
+ * numLiteralRustExpr a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
954
+ * `classifyExpr` leaves a bare number `dynamic` (its checked int/float range rules are the evaluate
955
+ * SSoT), so it never reaches the static str/bool/null branch — but a number literal IS statically
956
+ * resolvable to a native `Value`: the SAME range check the runtime applies (integral literal in the
957
+ * safe 2^53-1 window Value::Int; a fractional/exponent literal the checked Value::Float) is
958
+ * performed HERE at generation time. Returns the Rust expression for the literal's Value, or null
959
+ * when the node is not a bare number OR an integral literal is out of the safe window (keeping the
960
+ * runtime INVALID_LITERAL semantics — the port is not covered natively, the component falls through).
961
+ * Mirrors behavior_contracts eval of a bare number literal exactly.
962
+ */
963
+ const NUM_SAFE_INT_R = 9007199254740991; // 2^53 - 1 (expr safeInt)
964
+ function numLiteralRustExpr(node) {
965
+ if (typeof node !== "number" || !Number.isFinite(node))
966
+ return null;
967
+ if (Number.isInteger(node)) {
968
+ if (node < -NUM_SAFE_INT_R || node > NUM_SAFE_INT_R)
969
+ return null; // out of safe window → not covered
970
+ return `Value::Int(${node}i64)`;
971
+ }
972
+ // fractional / exponent literal → checked f64 (finite, guaranteed above).
973
+ return `Value::Float(${node}f64)`;
974
+ }
975
+ /** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
976
+ * resolvable to a native Value with no Obj / no evaluate interpreter. */
977
+ /** planForComp — a TypePlan for a single component (its named types are component-scoped). */
978
+ function planForComp(comp) {
979
+ return buildTypePlan({ irVersion: 1, exprVersion: 2, components: [comp] });
980
+ }
981
+ /** buildTypedNodes — the prior-node typed-cell TypeRef map (control-gate nodes typeless; a map node
982
+ * carries its produced-array ref). An untyped node is omitted. */
681
983
  function buildTypedNodes(comp, plan) {
682
984
  const typedNodes = new Map();
683
985
  for (const n of comp.body) {
684
986
  if (isControlGate(n))
685
987
  continue;
686
- // coverage probes under-typed components too; an unresolvable node just isn't a prior-node cell.
687
988
  try {
688
989
  typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
689
990
  }
@@ -693,6 +994,26 @@ function buildTypedNodes(comp, plan) {
693
994
  }
694
995
  return typedNodes;
695
996
  }
997
+ /** mapAsBinding — the `$as` element binding for a map node's element ports, or undefined (uncovered). */
998
+ function mapAsBinding(comp, node, typedNodes, plan) {
999
+ try {
1000
+ return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
1001
+ }
1002
+ catch {
1003
+ return undefined;
1004
+ }
1005
+ }
1006
+ /** portIsStatic — the ELIGIBILITY gate ASKS the one port lowering (compilePortNode / native-expr) whether
1007
+ * it can lower this port. A port that does not lower (or lowers fallibly) fails closed. */
1008
+ function portIsStatic(node, comp, plan, typedNodes, asBinding) {
1009
+ try {
1010
+ compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), "port", asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined);
1011
+ return true;
1012
+ }
1013
+ catch {
1014
+ return false;
1015
+ }
1016
+ }
696
1017
  /** The output must lower to a typed struct/value with no scope read: a ref to a BODY NODE, an
697
1018
  * obj/arr of such, or a concat. A ref to an input param / non-concat operator is not lowerable. */
698
1019
  function outputIsNativeLowerable(comp) {
@@ -724,7 +1045,7 @@ function outputIsNativeLowerable(comp) {
724
1045
  /**
725
1046
  * coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet), rust twin of the go predicate.
726
1047
  */
727
- function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
1048
+ function coveredMapNode(n, bodyIds, comp) {
728
1049
  if (n === null || typeof n !== "object" || !("map" in n))
729
1050
  return false;
730
1051
  const m = n.map;
@@ -756,25 +1077,17 @@ function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
756
1077
  return false;
757
1078
  }
758
1079
  const ports = m.ports;
1080
+ const plan = planForComp(comp);
1081
+ const typedNodes = buildTypedNodes(comp, plan);
759
1082
  const asBinding = mapAsBinding(comp, n, typedNodes, plan);
760
1083
  for (const p of Object.keys(ports))
761
1084
  if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
762
1085
  return false;
763
1086
  return true;
764
1087
  }
765
- /** the `$as` element binding for a covered map/fanout node's element ports (name + over-element TypeRef),
766
- * or undefined when the over element type does not resolve (the node is then uncovered upstream). */
767
- function mapAsBinding(comp, node, typedNodes, plan) {
768
- try {
769
- return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
770
- }
771
- catch {
772
- return undefined;
773
- }
774
- }
775
1088
  /** coveredFanoutNode (v3, rust twin) — a first-class fanout node is covered when its over is a covered
776
1089
  * ref (prior-node arr / input array with elemType) and its element ports are static. See the go twin. */
777
- function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
1090
+ function coveredFanoutNode(n, bodyIds, comp) {
778
1091
  if (n === null || typeof n !== "object" || !("fanout" in n))
779
1092
  return false;
780
1093
  const f = n.fanout;
@@ -794,6 +1107,8 @@ function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
794
1107
  return false;
795
1108
  }
796
1109
  const ports = f.ports;
1110
+ const plan = planForComp(comp);
1111
+ const typedNodes = buildTypedNodes(comp, plan);
797
1112
  let asBinding;
798
1113
  try {
799
1114
  asBinding = { name: f.as, ref: fanoutOverElemInfo(comp, n, typedNodes, plan).elemRef, plan };
@@ -838,8 +1153,8 @@ function fanoutOverElemInfo(comp, node, typedNodes, plan) {
838
1153
  }
839
1154
  /** A covered read SHAPE with static ports + native-lowerable output (+ covered map...into), OR a
840
1155
  * real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point reads. */
841
- function isSequentialComponentRefRead(comp, plan) {
842
- return coverageRejectReason(comp, plan) === null;
1156
+ function isSequentialComponentRefRead(comp) {
1157
+ return coverageRejectReason(comp) === null;
843
1158
  }
844
1159
  /**
845
1160
  * coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89), rust twin of
@@ -848,11 +1163,12 @@ function isSequentialComponentRefRead(comp, plan) {
848
1163
  * IDENTICAL to isSequentialComponentRefRead (defined as `coverageRejectReason(c) === null`) so the two
849
1164
  * can never disagree. This is the message the fail-closed dispatch surfaces per non-native component.
850
1165
  */
851
- function coverageRejectReason(comp, plan) {
1166
+ function coverageRejectReason(comp) {
1167
+ const crPlan = planForComp(comp);
1168
+ const crTypedNodes = buildTypedNodes(comp, crPlan);
852
1169
  const ep = execPlan(comp);
853
1170
  if (ep === null)
854
1171
  return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
855
- const typedNodes = buildTypedNodes(comp, plan);
856
1172
  // #114: a bindField relation child (single|connection) IS covered inside a parallel stage — the
857
1173
  // sibling hasMany fan-out (parent → members ∥ permissions). Its bound-key skip-gate is preflighted
858
1174
  // per member (from the settled parent) BEFORE dispatch, so the scoped-thread fan-out keeps
@@ -890,12 +1206,12 @@ function coverageRejectReason(comp, plan) {
890
1206
  continue;
891
1207
  }
892
1208
  if ("fanout" in n) {
893
- if (!coveredFanoutNode(n, bodyIds, comp, plan, typedNodes))
1209
+ if (!coveredFanoutNode(n, bodyIds, comp))
894
1210
  return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
895
1211
  continue;
896
1212
  }
897
1213
  if ("map" in n) {
898
- if (!coveredMapNode(n, bodyIds, comp, plan, typedNodes))
1214
+ if (!coveredMapNode(n, bodyIds, comp))
899
1215
  return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
900
1216
  continue;
901
1217
  }
@@ -914,7 +1230,7 @@ function coverageRejectReason(comp, plan) {
914
1230
  if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
915
1231
  return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
916
1232
  for (const p of Object.keys(cr.ports))
917
- if (portIsStatic(cr.ports[p], comp, plan, typedNodes) === false)
1233
+ if (portIsStatic(cr.ports[p], comp, crPlan, crTypedNodes) === false)
918
1234
  return `node '${n.id}' port '${p}' is not statically resolvable`;
919
1235
  }
920
1236
  if (!outputIsNativeLowerable(comp))
@@ -931,42 +1247,28 @@ function isFullyTyped(comp) {
931
1247
  return false;
932
1248
  return true;
933
1249
  }
934
- function isNativeRaw(comp, plan) {
935
- return isSequentialComponentRefRead(comp, plan) && isFullyTyped(comp);
1250
+ function isNativeRaw(comp) {
1251
+ return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
936
1252
  }
937
- function assertTypedCoverage(comp, plan) {
938
- if (!isSequentialComponentRefRead(comp, plan))
1253
+ function assertTypedCoverage(comp) {
1254
+ if (!isSequentialComponentRefRead(comp))
939
1255
  return;
940
1256
  if (isFullyTyped(comp))
941
1257
  return;
942
1258
  throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#77): component '${comp.name}' is a sequential componentRef read but lacks outType/outputType on every node — the combined emitter de-boxes the RESULT into the node's outType struct, so a covered read MUST be fully typed (bc#45). Add outType annotations, or regenerate on rust-straightline-native (boxed result).`);
943
1259
  }
944
1260
  // ── typed input struct (InNR<Comp>) + NATIVE scalar port lowering ────────────────────────────
945
- /**
946
- * inputPortRustType — the native Rust type for an input port's declared type. Scalars lower to the
947
- * concrete scalar; `array`/`map` with a declared elemType to `Vec<ElemT>` / `BTreeMap<String, ElemT>`;
948
- * an OPTIONAL port (`{opt:T}` → `required:false`) to `Option<T>` — the native representation of "the
949
- * value is absent". A declared type with no native lowering stays the boxed `Value`.
950
- *
951
- * An OPTIONAL port whose inner type has no native lowering FAILS CLOSED: `Option<Value>` would be a
952
- * boxed escape, and emitting the bare inner type would silently turn "absent" into a zero value. A port
953
- * whose optionality cannot be represented is never silently coerced.
954
- */
1261
+ /** Rust type for an input port's declared type (scalar → concrete; array+elemType → Vec<ElemT>; else Value). */
955
1262
  function inputPortRustType(schema, plan, where = "input port") {
956
1263
  const ref = inputPortTypeRef(schema, plan);
957
1264
  if (ref !== undefined)
958
1265
  return renderTypeRef(ref);
959
1266
  if (inputPortIsOptional(schema))
960
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Rust lowering, so "absent" has no native representation (an Option<Value> would be a boxed escape, and the bare inner type would silently read absent as a zero value). Declare an elemType for an array/map, or use a natively-lowerable scalar.`);
1267
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Rust lowering, so "absent" has no native representation. Declare an elemType for an array/map, or use a natively-lowerable scalar.`);
961
1268
  return "Value";
962
1269
  }
963
- /** The scalar kind a REQUIRED input port declares, or undefined when it is not a required native scalar.
964
- * An OPTIONAL port is deliberately NOT a scalar kind here: its native type is `Option<T>`, and the
965
- * callers of this function build REQUIRED scalar slots (`in_.<field>` read straight into a non-opt port
966
- * field). Answering "i64" for an `{opt:"int"}` port is exactly how "absent" would silently become `0`. */
1270
+ /** The scalar kind an input port declares, or undefined if it is not a native scalar. */
967
1271
  function inputScalarKind(schema) {
968
- if (inputPortIsOptional(schema))
969
- return undefined;
970
1272
  switch (schema?.type) {
971
1273
  case "string":
972
1274
  case "literal": // a `"literal"` union is a constrained String (see inputPortRustType).
@@ -990,6 +1292,8 @@ function emitInStruct(comp, plan) {
990
1292
  if (ports.length === 0) {
991
1293
  return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\n#[derive(Default)]\npub struct ${name};`;
992
1294
  }
1295
+ // #139: inputPortRustType reads the inputPortTypeRef SSoT — an OPTIONAL port is `Option<T>` (absent →
1296
+ // None); an optional whose inner type has no native lowering FAILS CLOSED (never a boxed escape).
993
1297
  const fields = ports
994
1298
  .map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`)}, // ${JSON.stringify(p)}`)
995
1299
  .join("\n");
@@ -1000,18 +1304,30 @@ pub struct ${name} {
1000
1304
  ${fields}
1001
1305
  }`;
1002
1306
  }
1003
- /** The port forms the STATIC (non-optional) lane lowers named in its reject message so the message
1004
- * describes what is actually covered rather than a stale corpus list. */
1307
+ /** A ref resolver for native lowering: given a ref head + remaining field path, return a native scalar
1308
+ * Rust expression (typed input field / prior-node typed struct scalar / element binding field), or null
1309
+ * when the ref does not resolve to a REQUIRED (non-opt) native scalar (then the caller falls back to the
1310
+ * Value path). Returned exprs are OWNED (String cloned / scalar copied) — usable in a `format!` / literal. */
1311
+ // bc#90 / runtime-free: the old `emitStaticR` / `inputLeafR` / `valueLeafR` port lowering (which built
1312
+ // `Value` leaves via the primitives::* helpers and struct-field serializers) is REMOVED — every covered
1313
+ // port now lowers to a FULLY NATIVE Rust value in emitPortInit (string / bool / static-string-array /
1314
+ // number literal), and a genuinely-dynamic port fails closed. There is no boxed-Value fallback on the
1315
+ // covered read plane.
1005
1316
  /**
1006
- * emitPortInit — the port field initializer, read off the ONE port lowering (compilePortNode / native-
1007
- * expr). The field's declared TYPE (portFieldRustType) is the SAME lowering's TypeRef, so type and value
1008
- * can never disagree. `isPrior` tests whether a ref head names a settled prior node at this position;
1009
- * `opts.asBinding`/`opts.asBase` carry a map/fanout element binding.
1317
+ * emitPortInit — emit the port field build for one port as a FULLY NATIVE Rust expression (bc#90 /
1318
+ * runtime-free). Every covered port lowers to a concrete Rust value assigned straight into the ports
1319
+ * struct field NO `Value`, NO fallback path, NO `match`:
1320
+ * - a static string array (projection) → `vec!["a", "b", ...]` (change #2);
1321
+ * - a bare int/float number literal (e.g. Query `limit: 20`) → `20i64` / `<n>f64`;
1322
+ * - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
1323
+ * A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered plane —
1324
+ * a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
1325
+ * ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
1010
1326
  */
1011
1327
  function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
1012
1328
  const c = compilePortNode(expr, comp, plan, typedNodes, isPrior, `port '${pn}'`, opts);
1013
1329
  if (c.stmts.length > 0)
1014
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): port '${pn}' ${JSON.stringify(expr)} lowers to a FALLIBLE expression (one that can raise INT_OVERFLOW / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS). Such an expression lowers to STATEMENTS carrying an early error-return, and a port is built inline in the ports-struct literal — a position that cannot host them. (A cond/guard position CAN host them, so the same expression is covered there — the boundary is the POSITION, not the operator.) Use a non-fallible port expression.`);
1330
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): port '${pn}' ${JSON.stringify(expr)} lowers to a FALLIBLE expression (INT_OVERFLOW / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS) statements with an early error-return, which a ports-struct literal cannot host. Use a non-fallible port expression.`);
1015
1331
  return `${portFieldName(pn)}: ${c.expr}`;
1016
1332
  }
1017
1333
  // ── the combined runner (STRUCT-returning; INLINE sequential; native ports in; concrete row out) ──
@@ -1108,11 +1424,11 @@ function emitNativeRawRunner(comp, plan, ap) {
1108
1424
  const lines = [];
1109
1425
  lines.push(`// run_native_raw_struct_${fn} — the STRUCT-RETURNING combined read (bc#77/#87/#94): the fully`);
1110
1426
  lines.push(`// de-plumbed CONCRETE path. Generic over the per-component CONCRETE ${handlerTraitName(comp.name)} trait,`);
1111
- lines.push(`// whose node_* methods take the node's native ports struct and return the concrete per-node row`);
1112
- lines.push(`// struct (typed fields = the outType). The runner builds each ports struct by direct native`);
1113
- lines.push(`// construction (a simple string port lowers to a native Rust expr), dispatches the concrete`);
1114
- lines.push(`// node_* method, and reads row.<field> DIRECTLY into the node's outType struct cell — no boxed`);
1115
- 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`);
1116
1432
  lines.push(`// results are typed struct cells; a relation child reads the parent's REAL struct result via`);
1117
1433
  lines.push(`// direct field access (child-present decision from the real parent value — relationSingle /`);
1118
1434
  lines.push(`// connection converge). A real-concurrency stage (bc#87) is static parallel orchestration —`);
@@ -1216,22 +1532,33 @@ function emitNativeRawRunner(comp, plan, ap) {
1216
1532
  // directly (no `.await`). The runner being `async fn` does not force every call site to await.
1217
1533
  const awaitSfx = ap.nodeIsAsync(node.id) ? ".await" : "";
1218
1534
  lines.push(`${indent}let ports_${s} = ${structName} { ${inits.join(", ")} };`);
1219
- lines.push(`${indent}let row_${s} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
1220
- lines.push(`${indent} Some(r) => r,`);
1221
- lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(node.component)})),`);
1222
- lines.push(`${indent}};`);
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).
1223
1538
  if (meta.policy === "continue") {
1224
- lines.push(`${indent}if !row_${s}.is_error {`);
1225
- lines.push(`${indent} ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1226
- lines.push(`${indent} produced_${s}.set(true);`);
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} }`);
1227
1553
  lines.push(`${indent}}`);
1228
1554
  }
1229
1555
  else {
1230
- const label = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1231
- lines.push(`${indent}if row_${s}.is_error {`);
1232
- lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under '${label}: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
1233
- lines.push(`${indent}}`);
1234
- lines.push(`${indent}${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1556
+ const policyLit = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1557
+ lines.push(`${indent}let wire_${s} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
1558
+ lines.push(`${indent} Ok(r) => r,`);
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 }),`);
1560
+ lines.push(`${indent}};`);
1561
+ lines.push(`${indent}*${typedCell(node.id)}.borrow_mut() = ${renderResultDeBox(ref, `wire_${s}`, node.id, plan, indent)};`);
1235
1562
  lines.push(`${indent}produced_${s}.set(true);`);
1236
1563
  }
1237
1564
  for (const c of closers)
@@ -1356,10 +1683,12 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1356
1683
  const nMembers = refMembers.length;
1357
1684
  const boundN = Math.min(concurrency, nMembers);
1358
1685
  const jobsVar = `jobs_${sanitize(comp.name)}`;
1686
+ const hWire = `<H as ${handlerTraitName(comp.name)}>::Wire`;
1359
1687
  for (const i of refMembers) {
1360
1688
  const s = sanitize(comp.body[i].id);
1361
- const rowT = rawRowStructName(comp.name, comp.body[i].id);
1362
- lines.push(` let slot_${s}: std::sync::Mutex<Option<Option<${rowT}>>> = 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);`);
1363
1692
  }
1364
1693
  lines.push(` let mut ${jobsVar}: Vec<usize> = Vec::new();`);
1365
1694
  refMembers.forEach((i, slot) => {
@@ -1397,22 +1726,29 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1397
1726
  const s = sanitize(node.id);
1398
1727
  const ref = typedNodes.get(node.id);
1399
1728
  lines.push(` if ports_${s}.is_some() {`);
1400
- lines.push(` let row_${s} = match slot_${s}.lock().unwrap().take() {`);
1401
- lines.push(` Some(Some(r)) => r,`);
1402
- lines.push(` _ => return Err(unknown_component(${rustStrLit(node.component)})),`);
1403
- lines.push(` };`);
1729
+ lines.push(` let out_${s} = slot_${s}.lock().unwrap().take().expect("runnable member wrote its slot");`);
1404
1730
  if (meta.policy === "continue") {
1405
- lines.push(` if !row_${s}.is_error {`);
1406
- lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1407
- lines.push(` produced_${s}.set(true);`);
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(` }`);
1408
1743
  lines.push(` }`);
1409
1744
  }
1410
1745
  else {
1411
- const label = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1412
- lines.push(` if row_${s}.is_error {`);
1413
- lines.push(` return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under '${label}: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
1414
- lines.push(` }`);
1415
- lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1746
+ const policyLit = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1747
+ lines.push(` let wire_${s} = match out_${s} {`);
1748
+ lines.push(` Ok(r) => r,`);
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 }),`);
1750
+ lines.push(` };`);
1751
+ lines.push(` *${typedCell(node.id)}.borrow_mut() = ${renderResultDeBox(ref, `wire_${s}`, node.id, plan, " ")};`);
1416
1752
  lines.push(` produced_${s}.set(true);`);
1417
1753
  }
1418
1754
  lines.push(` }`);
@@ -1449,18 +1785,28 @@ isAsync = false) {
1449
1785
  const portNames = Object.keys(m.ports);
1450
1786
  const elemArrRef = typedNodes.get(node.id);
1451
1787
  const elemName = renderTypeRef(elemArrRef.elem);
1452
- const asBinding = { name: asName, ref: overElemRef, plan };
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);
1453
1793
  // build the per-element CONCRETE native ports struct. `il` = indent inside the element loop.
1454
1794
  const buildPorts = (il) => {
1455
- const inits = portNames.map((pn) => emitPortInit(pn, m.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
1456
- return [`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`];
1795
+ const out = [];
1796
+ const inits = [];
1797
+ const asBinding = { name: asName, ref: overElemRef, plan };
1798
+ for (const pn of portNames) {
1799
+ inits.push(emitPortInit(pn, m.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
1800
+ }
1801
+ out.push(`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`);
1802
+ return out;
1457
1803
  };
1458
1804
  // #108 guard: compile `when` to a native bool over an element-scoped resolveHead ($as element value,
1459
1805
  // prior node, input port). On skip: `continue` (element excluded — matches run_behavior's keep-gate).
1460
1806
  const emitGuard = (il) => {
1461
1807
  if (!hasGuard)
1462
1808
  return [];
1463
- const resolveHead = nativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` });
1809
+ const resolveHead = nativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding: { name: asName, ref: overElemRef, plan }, asBase: `oel_${s}` });
1464
1810
  const g = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compileBool(m.when);
1465
1811
  const out = [];
1466
1812
  for (const st of g.stmts)
@@ -1500,27 +1846,26 @@ isAsync = false) {
1500
1846
  lines.push(`${indent} let want_${s} = items_${s}.len();`);
1501
1847
  lines.push(`${indent} let bp_${s} = ${batch} { items: items_${s} };`);
1502
1848
  lines.push(`${indent} let row_${s} = match handlers.${nodeMethodName(node.id)}(&bp_${s}, None)${awaitSfx} {`);
1503
- lines.push(`${indent} Some(r) => r,`);
1504
- lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(m.component)})),`);
1849
+ lines.push(`${indent} Ok(r) => r,`);
1850
+ lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
1505
1851
  lines.push(`${indent} };`);
1506
- lines.push(`${indent} if row_${s}.is_error {`);
1507
- lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
1508
- lines.push(`${indent} }`);
1509
- lines.push(`${indent} if row_${s}.rows.len() != want_${s} {`);
1852
+ lines.push(`${indent} if row_${s}.len() != want_${s} {`);
1510
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})));`);
1511
1854
  lines.push(`${indent} }`);
1512
1855
  if (hasInto) {
1513
- // zip the aligned batch rows onto each KEPT over element (into materializer).
1514
- lines.push(`${indent} let mut rows_iter_${s} = row_${s}.rows.into_iter();`);
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();`);
1515
1859
  lines.push(`${indent} for over_el_${s} in kept_${s}.iter() {`);
1516
1860
  lines.push(`${indent} let er_${s} = rows_iter_${s}.next().expect("aligned batch row");`);
1517
- 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}));`);
1518
1863
  lines.push(`${indent} }`);
1519
1864
  }
1520
1865
  else {
1521
- // no-into: collect each element DIRECTLY from the aligned per-element row (result = kept rows).
1522
- lines.push(`${indent} for er_${s} in row_${s}.rows.into_iter() {`);
1523
- 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} `)};`);
1524
1869
  lines.push(`${indent} built_${s}.push(el_${s});`);
1525
1870
  lines.push(`${indent} }`);
1526
1871
  }
@@ -1533,20 +1878,26 @@ isAsync = false) {
1533
1878
  lines.push(l);
1534
1879
  for (const l of buildPorts(indent + " "))
1535
1880
  lines.push(l);
1881
+ // Element Error Policy (scp-error.md): `skip` drops the failing element and proceeds (order is
1882
+ // preserved; the leaf keeps the Error Value it produced). `error` (default) promotes the element
1883
+ // Failure to the map's Component Failure, carrying the leaf's detail through.
1536
1884
  lines.push(`${indent} let er_${s} = match handlers.${nodeMethodName(node.id)}(&ep_${s}, None)${awaitSfx} {`);
1537
- lines.push(`${indent} Some(r) => r,`);
1538
- lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(m.component)})),`);
1885
+ lines.push(`${indent} Ok(r) => r,`);
1886
+ if (m.elementPolicy === "skip") {
1887
+ lines.push(`${indent} Err(_) => continue,`);
1888
+ }
1889
+ else {
1890
+ lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
1891
+ }
1539
1892
  lines.push(`${indent} };`);
1540
- lines.push(`${indent} if er_${s}.is_error {`);
1541
- lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(node.id)}, er_${s}.err)));`);
1542
- lines.push(`${indent} }`);
1893
+ // the element's wire is de-boxed strictly INLINE (fail-closed regardless of elementPolicy).
1894
+ lines.push(`${indent} let dec_${s} = ${renderElemDeBox(`${indent} `)};`);
1543
1895
  if (hasInto) {
1544
- lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s}, er_${s}));`);
1896
+ lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s}, dec_${s}));`);
1545
1897
  }
1546
1898
  else {
1547
1899
  lines.push(`${indent} let _ = &oel_${s};`);
1548
- lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
1549
- lines.push(`${indent} built_${s}.push(el_${s});`);
1900
+ lines.push(`${indent} built_${s}.push(dec_${s});`);
1550
1901
  }
1551
1902
  lines.push(`${indent}}`);
1552
1903
  }
@@ -1583,9 +1934,12 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
1583
1934
  if (!dkField || dkField.type.kind !== "scalar" || dkField.type.scalar !== "string")
1584
1935
  throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust fanout (v3): node '${node.id}' dedupeKey '${f.dedupeKey}' must be a string field of the connection items element type '${elemRef.name}'.`);
1585
1936
  const dkRust = rustFieldName(f.dedupeKey);
1586
- const asBinding = { name: asName, ref: overElemRef, plan };
1587
1937
  const buildPorts = (il) => {
1588
- const inits = portNames.map((pn) => emitPortInit(pn, f.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
1938
+ const inits = [];
1939
+ const asBinding = { name: asName, ref: overElemRef, plan };
1940
+ for (const pn of portNames) {
1941
+ inits.push(emitPortInit(pn, f.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
1942
+ }
1589
1943
  return [`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`];
1590
1944
  };
1591
1945
  const lines = [];
@@ -1609,21 +1963,24 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
1609
1963
  lines.push(`${indent} let want_${s} = pi_${s}.len();`);
1610
1964
  lines.push(`${indent} let bp_${s} = ${batch} { items: pi_${s} };`);
1611
1965
  lines.push(`${indent} let row_${s} = match handlers.${nodeMethodName(node.id)}(&bp_${s}, None)${awaitSfx} {`);
1612
- lines.push(`${indent} Some(r) => r,`);
1613
- lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(f.component)})),`);
1966
+ lines.push(`${indent} Ok(r) => r,`);
1967
+ lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
1614
1968
  lines.push(`${indent} };`);
1615
- lines.push(`${indent} if row_${s}.is_error {`);
1616
- lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
1617
- lines.push(`${indent} }`);
1618
- lines.push(`${indent} if row_${s}.rows.len() != want_${s} {`);
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.
1972
+ lines.push(`${indent} if row_${s}.len() != want_${s} {`);
1619
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})));`);
1620
1974
  lines.push(`${indent} }`);
1621
1975
  // ── THE ONE dedupe/drop (mirrors fanoutDedupDrop verbatim): first-seen dedupe by dedupeKey field;
1622
1976
  // drop dangling (empty dedupeKey ≡ interpreter's null/absent-key body); implicitSource strip is
1623
1977
  // structural (the elem type already omits it). cursor is always None (connection wrap).
1624
1978
  lines.push(`${indent} let mut seen_${s}: std::collections::HashSet<String> = std::collections::HashSet::new();`);
1625
- lines.push(`${indent} for er_${s} in row_${s}.rows.into_iter() {`);
1626
- lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemRef, plan)}`);
1979
+ lines.push(`${indent} for opt_er_${s} in row_${s}.into_iter() {`);
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} `)},`);
1982
+ lines.push(`${indent} None => Default::default(),`);
1983
+ lines.push(`${indent} };`);
1627
1984
  lines.push(`${indent} let key_${s} = el_${s}.${dkRust}.clone();`);
1628
1985
  if (f.drop === "dangling") {
1629
1986
  lines.push(`${indent} if key_${s}.is_empty() { continue; }`);
@@ -1639,73 +1996,6 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
1639
1996
  lines.push(c);
1640
1997
  return lines.join("\n");
1641
1998
  }
1642
- /**
1643
- * rustOwn — render the OWNED form of a resolved ref value at the LEAF (native-expr calls this after the
1644
- * field path is walked). `guard` marks a value read through a `RefCell`/`&` borrow (a prior node's
1645
- * `cell.borrow()`, a `$as` element `&Elem`): a WHOLE-node Copy scalar dereferences (`*expr`), while a
1646
- * Copy FIELD is already a copy-out of the auto-deref'd place. A String/struct/map clones; an array clones
1647
- * only when the position OWNS it (a port field) and stays borrowed for a `len`. Cloning a Copy value
1648
- * would trip clippy's `clone_on_copy`, so Copy never clones.
1649
- */
1650
- function rustOwn(guard, ownArrays) {
1651
- return (expr, ref, fieldAccessed) => {
1652
- if (ref.kind === "arr")
1653
- return ownArrays ? `${expr}.clone()` : expr;
1654
- if (typeRefIsCopy(ref))
1655
- return guard && !fieldAccessed ? `*${expr}` : expr;
1656
- return `${expr}.clone()`;
1657
- };
1658
- }
1659
- /**
1660
- * nativeResolveHead — the ONE ref-head resolver the runtime-free native-expr compiler reads, for cond
1661
- * `if`/branches, a map/fanout `when` guard, AND a port. A head is a `$as` element binding, a prior body
1662
- * node's typed cell, or an input port — each a native base expr of the head's TypeRef PLUS an `own`
1663
- * renderer (rustOwn) that native-expr applies at the resolved LEAF. Ownership lives at the leaf so a
1664
- * prior-node field ref clones only the field (`cell.borrow().items.clone()`), never the whole struct.
1665
- * `isPrior` tests whether a head names a settled prior node (position-aware at a runner site; structural
1666
- * at coverage/struct-def). `asBase` is the element base for a `$as`-headed ref; `ownArrays` owns an array
1667
- * (a port field) vs borrows it (a cond `len`).
1668
- */
1669
- function nativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
1670
- const ownArrays = opts?.ownArrays === true;
1671
- return (head) => {
1672
- // a `$as` element (`for oel in vec.iter()`) is a `&Elem` — a borrow guard.
1673
- if (opts?.asBinding !== undefined && opts.asBase !== undefined && head === opts.asBinding.name) {
1674
- return { expr: opts.asBase, ref: opts.asBinding.ref, own: rustOwn(true, ownArrays) };
1675
- }
1676
- // a prior node's value lives behind a `RefCell` borrow guard.
1677
- if (isPrior(head)) {
1678
- const ref = typedNodes.get(head);
1679
- return { expr: `${typedCell(head)}.borrow()`, ref, own: rustOwn(true, ownArrays) };
1680
- }
1681
- // an input port is a by-value struct field (no guard).
1682
- const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
1683
- if (ref === undefined)
1684
- return null;
1685
- return { expr: `in_.${rustFieldName(head)}`, ref, own: rustOwn(false, ownArrays) };
1686
- };
1687
- }
1688
- /**
1689
- * compilePortNode — lower a port expression to its native ports-struct field via the runtime-free
1690
- * native-expr compiler (native-expr.ts): the ONE port lowering. The port's field TYPE is the compiled
1691
- * `ref`, its initializer the compiled `expr` — so the two are derived from a SINGLE lowering and can
1692
- * never disagree. Any closed operator, a literal, a ref (input scalar/array/map, prior-node scalar/array,
1693
- * `$as` element field), a concat, a static array, an optional-input read — all resolve here. A port that
1694
- * cannot lower to a native value FAILS CLOSED loudly. A lowering that is FALLIBLE (hoists statements) is
1695
- * a covered SHAPE returned here as-is; only the value site (emitPortInit) rejects it, since a ports-struct
1696
- * literal cannot host statements — the coverage gate/type site must still see it as covered.
1697
- */
1698
- function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
1699
- const resolveHead = nativeResolveHead(comp, plan, typedNodes, isPrior, { ...opts, ownArrays: true });
1700
- try {
1701
- return new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compilePort(node);
1702
- }
1703
- catch (e) {
1704
- if (!(e instanceof GeneratorFailure))
1705
- throw e;
1706
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} ${JSON.stringify(node)} does not lower to a native Rust value: ${e.message}`);
1707
- }
1708
- }
1709
1999
  /**
1710
2000
  * emitCondArm — the struct-native exec of a covered cond node (#108, rust). Mirrors run_behavior's cond
1711
2001
  * lower: compile `if` to a native bool, materialize the TAKEN branch to the node's outType (only the
@@ -1768,16 +2058,6 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
1768
2058
  lines.push(cl);
1769
2059
  return lines.join("\n");
1770
2060
  }
1771
- /** emitElemMove — move a concrete element row (RawElemNR, error already handled) into a fresh `let` local
1772
- * of the element outType. Named → struct literal from moved fields; scalar/arr/opt → .val. */
1773
- function emitElemMove(letBind, rowExpr, ref, plan) {
1774
- if (ref.kind === "named") {
1775
- const decl = findDecl(plan, ref.name);
1776
- const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: ${rowExpr}.${rustFieldName(f.name)}`).join(", ");
1777
- return `${letBind} = ${ref.name} { ${inits} };`;
1778
- }
1779
- return `${letBind} = ${rowExpr}.val;`;
1780
- }
1781
2061
  /**
1782
2062
  * emitProducedAwareValue — the #86 part 1 produced-aware output lowering (rust twin of the go one).
1783
2063
  */
@@ -1847,6 +2127,13 @@ pub const COMPONENT_NAMES_NATIVE_RAW: [&str; ${names.length}] = [${names.join(",
1847
2127
  const UNKNOWN_COMPONENT_HELPER = `#[allow(dead_code)]
1848
2128
  fn unknown_component(component: &str) -> BehaviorError {
1849
2129
  BehaviorError::new("UNKNOWN_COMPONENT", format!("component '{component}' has no handler (fail-closed)"))
2130
+ }
2131
+
2132
+ // leaf_failure — a failure the LEAF reports (test glue): the runner assigns the node's failure code
2133
+ // from its Error Policy Kind, so what a leaf carries is its message and its structured Error Value.
2134
+ #[allow(dead_code)]
2135
+ fn leaf_failure(message: impl Into<String>) -> BehaviorError {
2136
+ BehaviorError::new("LEAF_FAILURE", message)
1850
2137
  }`;
1851
2138
  // ── module assembly ──────────────────────────────────────────────────────────────────
1852
2139
  function emit(ctx) {
@@ -1855,20 +2142,17 @@ function emit(ctx) {
1855
2142
  // rust CONSUMES this annotation (async fn + .await where derived); go ignores it. per-node granularity
1856
2143
  // is a strict generalization of increment-1's uniform ioModel:'async' (= every terminal async).
1857
2144
  const asyncPlans = buildAsyncPlans(ctx.ir, ctx.ioModel);
1858
- // The type plan is the SSoT the coverage gate + the port lowering both read (the gate ASKS the port
1859
- // lowering whether a port lowers), so it is built up-front — before the coverage checks.
1860
- const plan = buildTypePlan(ctx.ir);
1861
2145
  for (const c of ctx.ir.components)
1862
- assertTypedCoverage(c, plan);
1863
- const native = ctx.ir.components.filter((c) => isNativeRaw(c, plan));
1864
- const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c, plan));
2146
+ assertTypedCoverage(c);
2147
+ const native = ctx.ir.components.filter(isNativeRaw);
2148
+ const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
1865
2149
  // FAIL CLOSED (bc#86/#89): the --typed-native (native codegen) surface is native-only. Previously a
1866
2150
  // non-covered component was SILENTLY DROPPED (emitted only as a comment note), concealing that it
1867
2151
  // never got native codegen. We now throw a LOUD GeneratorFailure naming every uncovered component +
1868
2152
  // its specific reject reason. A fully-covered module is unchanged.
1869
2153
  if (nonNative.length > 0) {
1870
2154
  const rejects = nonNative
1871
- .map((c) => ` - component '${c.name}': ${coverageRejectReason(c, plan) ?? "not a covered native read"}`)
2155
+ .map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
1872
2156
  .join("\n");
1873
2157
  throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (native codegen) surface: ${nonNative.length} component(s) are NOT a covered native read:\n${rejects}\n\n` +
1874
2158
  `The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
@@ -1882,6 +2166,7 @@ function emit(ctx) {
1882
2166
  ${emitStructSurface([])}
1883
2167
  `;
1884
2168
  }
2169
+ const plan = buildTypePlan(ctx.ir);
1885
2170
  // The typed-native module is a de-boxed CONSUMER artifact: the covered runner RETURNS these outType
1886
2171
  // structs and the consumer reads their fields natively. So the decls (and their FIELDS) are pub — the
1887
2172
  // consumer keeps the model native (mirrors Go's exported outType struct fields on the covered path).
@@ -1894,12 +2179,18 @@ ${emitStructSurface([])}
1894
2179
  // #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler).
1895
2180
  rustExprUsed.helpers = false;
1896
2181
  const structs = [];
1897
- const rowStructs = [];
1898
2182
  const handlerTraits = [];
1899
2183
  const inStructs = [];
1900
2184
  const elemMaterializers = [];
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)));
1901
2188
  for (const c of native) {
1902
- const typedNodes = buildTypedNodes(c, plan);
2189
+ const typedNodes = new Map();
2190
+ // build the full typed-node map FIRST so a map's over-element resolution can see every node.
2191
+ for (const n of c.body)
2192
+ if (!isControlGate(n))
2193
+ typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan)); // #125: control-gate typeless; map は produced-array ref に正規化
1903
2194
  for (const n of c.body) {
1904
2195
  if ("fanout" in n)
1905
2196
  structs.push(emitFanoutPortsStructs(c, n, typedNodes, plan));
@@ -1909,9 +2200,8 @@ ${emitStructSurface([])}
1909
2200
  // #108: a cond node has NO ports struct (pure Expression — no handler dispatch).
1910
2201
  }
1911
2202
  else
1912
- structs.push(emitPortsStruct(c, n, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
2203
+ structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
1913
2204
  }
1914
- rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
1915
2205
  handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
1916
2206
  inStructs.push(emitInStruct(c, plan));
1917
2207
  const mm = emitMapElemMaterializers(c, typedNodes, plan);
@@ -1927,9 +2217,9 @@ ${emitStructSurface([])}
1927
2217
  // relationKind:single children), emitted ONLY as a STRUCT-returning runner GENERIC over a per-component
1928
2218
  // CONCRETE handler trait (HandlerNR<comp>): native ports struct IN (direct construction — every port is a
1929
2219
  // CONCRETE Rust value: String / bool / Vec<&str> projection / i64 limit — NO boxed Value, NO by-name
1930
- // accessor); the handler takes a typed produced-aware 'bound' Option and returns the node's CONCRETE row
1931
- // struct (typed fields = the projected outType), and the runner reads row.<field> DIRECTLY into the node's
1932
- // 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
1933
2223
  // BehaviorError (same codes, byte-equal to run_behavior). INLINE sequential (no plan driver) so a relation
1934
2224
  // child reads the parent's REAL struct result via direct field access and the child-present decision is
1935
2225
  // made from the real parent value — relationSingle / connection converge with run_behavior (fixes
@@ -1950,11 +2240,11 @@ use std::cell::RefCell;`;
1950
2240
  structs.some((r) => r.trim().length > 0)
1951
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")}`
1952
2242
  : undefined,
1953
- rowStructs.some((r) => r.trim().length > 0)
1954
- ? `// CONCRETE per-node row structs (typed fields = the node's outType; + error signal).\n${rowStructs.filter((r) => r.trim().length > 0).join("\n\n")}`
1955
- : undefined,
1956
2243
  `// CONCRETE per-component input structs (fields = inputPorts).\n${inStructs.join("\n\n")}`,
1957
- `// CONCRETE per-component handler traits (one node_* method per node — native ports IN, row OUT).\n${handlerTraits.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()}`
2247
+ : undefined,
1958
2248
  elemMaterializers.length
1959
2249
  ? `// map augmented-element copiers (pure struct field assignment — no dynamic value read).\n${elemMaterializers.join("\n\n")}`
1960
2250
  : undefined,
@@ -1974,8 +2264,8 @@ use std::cell::RefCell;`;
1974
2264
  */
1975
2265
  export function rustTypedNativeObserve(ir, ioModel = "sync") {
1976
2266
  const components = ir.components;
2267
+ const native = components.filter(isNativeRaw);
1977
2268
  const plan = buildTypePlan(ir);
1978
- const native = components.filter((c) => isNativeRaw(c, plan));
1979
2269
  const needSync = native.some((c) => concurrencyPlan(c) !== null);
1980
2270
  const serializers = emitSerializers(plan);
1981
2271
  const marshallers = emitMarshallers(plan);
@@ -1986,10 +2276,182 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
1986
2276
  const anyAsyncRunner = native.some((c) => asyncPlans.get(c.name).runnerIsAsync);
1987
2277
  // ── per-component input decoders: generic &[(String,Value)] -> concrete InNR_<comp> (test glue). ──
1988
2278
  const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
1989
- // ── concrete scripted handler: implements every HandlerNR<comp> trait by decoding a scripted Value
1990
- // into the concrete per-node row (the generic scripted-source queue meets the concrete seam). ──
1991
- const decodeFns = [];
1992
- const emittedDecode = new Set();
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.
2287
+ const wireAdapter = anyDeBox
2288
+ ? `// ── scripted wire adapter (TEST glue) ──
2289
+ fn scripted_wire_tag(v: &Value) -> String {
2290
+ match v {
2291
+ Value::Null => "NULL",
2292
+ Value::Bool(_) => "BOOL",
2293
+ Value::Int(_) | Value::Float(_) => "N",
2294
+ Value::Str(_) => "S",
2295
+ Value::Arr(_) => "L",
2296
+ Value::Obj(_) => "M",
2297
+ }
2298
+ .to_string()
2299
+ }
2300
+
2301
+ fn scripted_wire_raw(v: &Value) -> String {
2302
+ match v {
2303
+ Value::Null => "null".to_string(),
2304
+ Value::Bool(b) => b.to_string(),
2305
+ Value::Int(n) => n.to_string(),
2306
+ Value::Float(f) => f.to_string(),
2307
+ Value::Str(s) => s.clone(),
2308
+ Value::Arr(_) | Value::Obj(_) => "[composite]".to_string(),
2309
+ }
2310
+ }
2311
+
2312
+ fn probe_string_val(v: Option<&Value>) -> Probe<String> {
2313
+ match v {
2314
+ None => Probe::Absent,
2315
+ Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
2316
+ Some(Value::Str(s)) => Probe::Got(s.clone()),
2317
+ Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
2318
+ }
2319
+ }
2320
+
2321
+ fn probe_number_val(v: Option<&Value>) -> NumProbe {
2322
+ match v {
2323
+ None => NumProbe::Absent,
2324
+ Some(Value::Null) => NumProbe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
2325
+ Some(Value::Int(n)) => NumProbe::Got { raw: n.to_string(), actual_wire_type: "N".to_string() },
2326
+ Some(Value::Float(f)) => NumProbe::Got { raw: f.to_string(), actual_wire_type: "N".to_string() },
2327
+ Some(other) => NumProbe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
2328
+ }
2329
+ }
2330
+
2331
+ fn probe_bool_val(v: Option<&Value>) -> Probe<bool> {
2332
+ match v {
2333
+ None => Probe::Absent,
2334
+ Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
2335
+ Some(Value::Bool(b)) => Probe::Got(*b),
2336
+ Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
2337
+ }
2338
+ }
2339
+
2340
+ fn probe_row_val(v: Option<&Value>) -> Probe<ScriptedWireRow> {
2341
+ match v {
2342
+ None => Probe::Absent,
2343
+ Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
2344
+ Some(Value::Obj(pairs)) => Probe::Got(ScriptedWireRow { obj: pairs.clone() }),
2345
+ Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
2346
+ }
2347
+ }
2348
+
2349
+ fn probe_list_val(v: Option<&Value>) -> Probe<ScriptedWireList> {
2350
+ match v {
2351
+ None => Probe::Absent,
2352
+ Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
2353
+ Some(Value::Arr(items)) => Probe::Got(ScriptedWireList { items: items.clone() }),
2354
+ Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
2355
+ }
2356
+ }
2357
+
2358
+ pub struct ScriptedWireRow {
2359
+ obj: Vec<(String, Value)>,
2360
+ }
2361
+
2362
+ impl ScriptedWireRow {
2363
+ pub fn new(v: Value) -> Self {
2364
+ match v {
2365
+ Value::Obj(pairs) => ScriptedWireRow { obj: pairs },
2366
+ _ => ScriptedWireRow { obj: Vec::new() },
2367
+ }
2368
+ }
2369
+ fn get(&self, field: &str) -> Option<&Value> {
2370
+ self.obj.iter().find(|(k, _)| k.as_str() == field).map(|(_, v)| v)
2371
+ }
2372
+ }
2373
+
2374
+ pub struct ScriptedWireList {
2375
+ items: Vec<Value>,
2376
+ }
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
+
2409
+ impl WireRow for ScriptedWireRow {
2410
+ type List = ScriptedWireList;
2411
+ fn keys(&self) -> Vec<String> {
2412
+ self.obj.iter().map(|(k, _)| k.clone()).collect()
2413
+ }
2414
+ fn probe_string(&self, field: &str) -> Probe<String> {
2415
+ probe_string_val(self.get(field))
2416
+ }
2417
+ fn probe_number(&self, field: &str) -> NumProbe {
2418
+ probe_number_val(self.get(field))
2419
+ }
2420
+ fn probe_bool(&self, field: &str) -> Probe<bool> {
2421
+ probe_bool_val(self.get(field))
2422
+ }
2423
+ fn probe_row(&self, field: &str) -> Probe<Self> {
2424
+ probe_row_val(self.get(field))
2425
+ }
2426
+ fn probe_list(&self, field: &str) -> Probe<Self::List> {
2427
+ probe_list_val(self.get(field))
2428
+ }
2429
+ }
2430
+
2431
+ impl WireList for ScriptedWireList {
2432
+ type Row = ScriptedWireRow;
2433
+ fn len(&self) -> usize {
2434
+ self.items.len()
2435
+ }
2436
+ fn elem_string(&self, i: usize) -> Probe<String> {
2437
+ probe_string_val(self.items.get(i))
2438
+ }
2439
+ fn elem_number(&self, i: usize) -> NumProbe {
2440
+ probe_number_val(self.items.get(i))
2441
+ }
2442
+ fn elem_bool(&self, i: usize) -> Probe<bool> {
2443
+ probe_bool_val(self.items.get(i))
2444
+ }
2445
+ fn elem_row(&self, i: usize) -> Probe<Self::Row> {
2446
+ probe_row_val(self.items.get(i))
2447
+ }
2448
+ fn elem_list(&self, i: usize) -> Probe<Self> {
2449
+ probe_list_val(self.items.get(i))
2450
+ }
2451
+ }`
2452
+ : "";
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). ──
1993
2455
  const traitImpls = [];
1994
2456
  for (const c of native) {
1995
2457
  const ap = asyncPlans.get(c.name);
@@ -2009,101 +2471,106 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
2009
2471
  // Option<K> (NOT a boxed Value) — computed per node so the impl signature matches the trait.
2010
2472
  const bt = boundRustType(n, typedNodes, plan);
2011
2473
  if ("fanout" in n) {
2012
- // v3: scripted fanout impl drain one batched outcome, decode the aligned array into per-body
2013
- // elem rows (a null array element decodes to the zero elem row = dangling dropped by the arm).
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).
2014
2476
  const f = n.fanout;
2015
- const elemRowRef = fanoutItemsElemRef(n, ref, plan);
2016
- const elemT = rawElemStructName(c.name, n.id);
2017
- const batchT = rawRowStructName(c.name, n.id);
2018
- const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2019
2477
  const portsT = `${portsStructName(c.name, n.id)}Batch`;
2020
- methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
2021
- let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)})?;
2478
+ methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<Option<ScriptedWireValue>>, BehaviorError> {
2479
+ let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)}).ok_or_else(|| unknown_component(${rustStrLit(f.component)}))?;
2022
2480
  if is_err {
2023
- return Some(${batchT} { is_error: true, err, ..Default::default() });
2481
+ return Err(leaf_failure(err));
2024
2482
  }
2025
2483
  let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
2026
- // v3: a null aligned body is a DANGLING id — decode it to the zero elem row (whose dedupeKey
2027
- // field is empty), so the runner's dangling-drop excludes it (byte-equal to fanoutDedupDrop).
2028
- let rows = arr
2029
- .iter()
2484
+ let wires = arr
2485
+ .into_iter()
2030
2486
  .map(|ev| match ev {
2031
- Value::Null => ${elemT}::default(),
2032
- _ => ${elemDecode}(ev),
2487
+ Value::Null => None,
2488
+ _ => Some(ScriptedWireValue::new(ev)),
2033
2489
  })
2034
2490
  .collect();
2035
- Some(${batchT} { rows, ..Default::default() })
2491
+ Ok(wires)
2036
2492
  }`);
2037
2493
  continue;
2038
2494
  }
2039
2495
  if ("map" in n) {
2496
+ // map: batched → one WIRE per element (Vec<ScriptedWireValue>); non-batched → one element WIRE.
2040
2497
  const m = n.map;
2041
2498
  const batched = m.batched === true;
2042
- const arrRef = ref;
2043
- const elemRowRef = mapElemRowRef(n, arrRef, plan);
2044
- const elemT = rawElemStructName(c.name, n.id);
2045
- const batchT = rawRowStructName(c.name, n.id);
2046
- const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2047
2499
  const portsT = batched ? `${portsStructName(c.name, n.id)}Batch` : portsStructName(c.name, n.id);
2048
- const retT = batched ? batchT : elemT;
2049
2500
  if (batched) {
2050
- methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
2051
- let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)})?;
2501
+ methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<ScriptedWireValue>, BehaviorError> {
2502
+ let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
2052
2503
  if is_err {
2053
- return Some(${batchT} { is_error: true, err, ..Default::default() });
2504
+ return Err(leaf_failure(err));
2054
2505
  }
2055
2506
  let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
2056
- let rows = arr.iter().map(${elemDecode}).collect();
2057
- Some(${batchT} { rows, ..Default::default() })
2507
+ let wires = arr.into_iter().map(ScriptedWireValue::new).collect();
2508
+ Ok(wires)
2058
2509
  }`);
2059
2510
  }
2060
2511
  else {
2061
- methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${elemT}> {
2062
- let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)})?;
2512
+ methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<ScriptedWireValue, BehaviorError> {
2513
+ let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
2063
2514
  if is_err {
2064
- return Some(${elemT} { is_error: true, err, ..Default::default() });
2515
+ return Err(leaf_failure(err));
2065
2516
  }
2066
- let v = ok.unwrap_or(Value::Null);
2067
- Some(${elemDecode}(&v))
2517
+ Ok(ScriptedWireValue::new(ok.unwrap_or(Value::Null)))
2068
2518
  }`);
2069
2519
  }
2070
- void retT;
2071
2520
  continue;
2072
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.
2073
2524
  const node = n;
2074
- const rowT = rawRowStructName(c.name, node.id);
2075
- const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
2076
- // #129 (TEST glue): support the reference scriptedHandlers' echo forms (PROTOCOL.md §3.5) — the
2077
- // adapter normalizes `{"echo":"port","port":"X"}` / `{"echo":"items"}` to the port NAME, and the
2078
- // scripted handler
2079
- // ECHOES a NATIVE port back as its result. This makes a port-plane pin non-vacuous: the node's
2080
- // observed result IS the native value that flowed into its ports struct, so a dropped / re-boxed /
2081
- // zero-defaulted port diverges from run_behavior instead of passing. A port is echoable when its
2082
- // native type IS the node's outType (that is exactly when the row's `val` can hold it); `ports` is
2083
- // bound only when at least one port qualifies.
2084
- const echoable = echoablePorts(c, node, ref, plan, typedNodes);
2085
- const portsParam = echoable.length > 0 ? "ports" : "_ports";
2086
- // a Copy value (i64/f64/bool/Option<scalar>) is read directly — cloning it trips clippy clone_on_copy.
2087
- const own = (expr, r) => `${expr}${typeRefIsCopy(r) ? "" : ".clone()"}`;
2088
- // a NAMED outType row is FLATTENED (no `val`) — echo a named port by copying the struct's fields;
2089
- // a scalar/arr/opt outType row holds the port in its single `val` field.
2090
- const echoRow = (p) => ref.kind === "named"
2091
- ? `${rowT} { ${findDecl(plan, ref.name).fields.map((f) => `${rustFieldName(f.name)}: ${own(`${portsParam}.${portFieldName(p)}.${rustFieldName(f.name)}`, f.type)}`).join(", ")}, ..Default::default() }`
2092
- : `${rowT} { val: ${own(`${portsParam}.${portFieldName(p)}`, ref)}, ..Default::default() }`;
2093
- const echoBranch = echoable
2094
- .map((p) => `\n if echo == ${rustStrLit(p)} {\n return Some(${echoRow(p)});\n }`)
2095
- .join("");
2096
- const okBind = echoable.length > 0 ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
2097
- methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
2098
- let ${okBind} = self.next(${rustStrLit(node.component)})?;
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}'`);
2537
+ }
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 }`);
2550
+ }
2551
+ }
2552
+ }
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;
2560
+ const portsParam = canEcho ? "ports" : "_ports";
2561
+ const okBind = canEcho ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
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> {
2564
+ let ${okBind} = self.next(${rustStrLit(node.component)}).ok_or_else(|| unknown_component(${rustStrLit(node.component)}))?;
2099
2565
  if is_err {
2100
- return Some(${rowT} { is_error: true, err, ..Default::default() });
2101
- }${echoBranch}
2102
- let v = ok.unwrap_or(Value::Null);
2103
- Some(${decode}(&v))
2566
+ return Err(leaf_failure(err));
2567
+ }
2568
+ ${echoBlock} Ok(ScriptedWireValue::new(ok.unwrap_or(Value::Null)))
2104
2569
  }`);
2105
2570
  }
2106
- traitImpls.push(`impl ${handlerTraitName(c.name)} for ScriptedNativeRaw {\n${methods.join("\n")}\n}`);
2571
+ const cHasHandler = c.body.some((n) => !("cond" in n));
2572
+ const wireAssoc = cHasHandler ? ` type Wire = ScriptedWireValue;\n` : "";
2573
+ traitImpls.push(`impl ${handlerTraitName(c.name)} for ScriptedNativeRaw {\n${wireAssoc}${methods.join("\n")}\n}`);
2107
2574
  }
2108
2575
  const arms = native
2109
2576
  .map((c) => {
@@ -2152,7 +2619,7 @@ impl ScriptedNativeRaw {
2152
2619
  }
2153
2620
 
2154
2621
  // drain one scripted outcome for a component (last entry repeats — mirrors the reference scripted
2155
- // handlers); None when the component has no scripted queue (fail-closed). Returns (is_error, err,
2622
+ // handlers); None when the component has no scripted queue. Returns (is_error, err,
2156
2623
  // ok_value, echo). The bc#87 parallel-stage runner calls node_* from multiple threads, so the drain
2157
2624
  // is Mutex-guarded. #129: \`echo\` carries the reference scriptedHandlers' \`echo\` directive (e.g.
2158
2625
  // "items") so a scripted node can ECHO a named input PORT back — used to make the node→leaf array
@@ -2162,13 +2629,11 @@ impl ScriptedNativeRaw {
2162
2629
  let mut queues = self.queues.lock().unwrap();
2163
2630
  let (queue, single) = queues.get_mut(component)?;
2164
2631
  let raw = if *single || queue.len() <= 1 { queue[0].clone() } else { queue.remove(0) };
2165
- // PROTOCOL.md §3.5 echo forms, normalized to the port NAME: {"echo":"port","port":"X"} -> "X"
2166
- // ({"echo":"items"} is already that shape). The generated node method just compares this
2167
- // against its echoable port names.
2168
- let echo = match raw.get("echo").and_then(|e| e.as_str()) {
2169
- Some("port") => raw.get("port").and_then(|p| p.as_str()).unwrap_or("").to_string(),
2170
- Some(other) => other.to_string(),
2171
- None => String::new(),
2632
+ // normalize echo:port to the port NAME X so a de-box handler's echo comparison fires;
2633
+ // echo:items already names the port directly.
2634
+ let echo = {
2635
+ let e = raw.get("echo").and_then(|e| e.as_str()).unwrap_or("");
2636
+ if e == "port" { raw.get("port").and_then(|p| p.as_str()).unwrap_or("").to_string() } else { e.to_string() }
2172
2637
  };
2173
2638
  if let Some(okv) = raw.get("ok") {
2174
2639
  let v = behavior_contracts::decode_value(okv).expect("decode ok");
@@ -2196,9 +2661,27 @@ use behavior_contracts::Value;
2196
2661
  use super::*;
2197
2662
 
2198
2663
  // bridge the covered module's LOCAL BehaviorError (super::BehaviorError) to the bc-runtime failure type
2199
- // the conformance harness reads (same code + message) — the ONLY place the two meet (test glue).
2664
+ // the conformance harness reads (same code + message + structured detail) — the ONLY place the two meet
2665
+ // (test glue). The de-box's structured Error Value is carried over so the harness can assert it.
2200
2666
  fn to_rt_err(e: BehaviorError) -> behavior_contracts::BehaviorError {
2201
- behavior_contracts::BehaviorError::new(e.code, e.message)
2667
+ let mut rt = behavior_contracts::BehaviorError::new(e.code, e.message);
2668
+ if let Some(d) = e.detail {
2669
+ let d = *d;
2670
+ rt.detail = Some(Box::new(behavior_contracts::ErrorDetail {
2671
+ kind: d.kind.map(|k| match k {
2672
+ ErrorKind::TypeMismatch => behavior_contracts::ErrorKind::TypeMismatch,
2673
+ ErrorKind::MissingField => behavior_contracts::ErrorKind::MissingField,
2674
+ ErrorKind::Overflow => behavior_contracts::ErrorKind::Overflow,
2675
+ }),
2676
+ model: d.model,
2677
+ field: d.field,
2678
+ expected_type: d.expected_type,
2679
+ actual_wire_type: d.actual_wire_type,
2680
+ raw_value: d.raw_value,
2681
+ context: d.context,
2682
+ }));
2683
+ }
2684
+ rt
2202
2685
  }
2203
2686
  ${anyAsyncRunner
2204
2687
  ? `// bc#97 (TEST glue) — a minimal std-only block-on that drives an async runner Future to completion so
@@ -2232,12 +2715,12 @@ ${marshallers}
2232
2715
 
2233
2716
  ${emitMarshalHelpers()}
2234
2717
 
2235
- // per-node concrete-row decoders (scripted Value -> RawRowNR_/RawElemNR_ struct).
2236
- ${decodeFns.join("\n\n")}
2237
-
2238
2718
  // input decoders (generic &[(String,Value)] -> concrete InNR_<comp>; TEST glue, one decode at the boundary).
2239
2719
  ${inDecoders}
2240
2720
 
2721
+ // scripted wire adapter (TEST glue) — wraps a scripted Value as a WireValue the generated inline de-box probes.
2722
+ ${wireAdapter}
2723
+
2241
2724
  ${adapter}
2242
2725
 
2243
2726
  // scripted handler trait impls (one per covered component — the concrete node_* seam, test glue).
@@ -2260,10 +2743,8 @@ ${arms}
2260
2743
  }
2261
2744
  /** emitInDecoder — a TEST-glue decoder `decode_InNR_<comp>(input) -> Result<InNR_<comp>, BehaviorError>`.
2262
2745
  * Reads each declared input port from the generic &[(String,Value)] into the concrete field (scalar →
2263
- * typed read; opt → Some/None; Value-typed → the raw Value clone). A key that is absent, or present as
2264
- * `Value::Null`, leaves the field's Default for an OPTIONAL port that is `None` (absent), which is the
2265
- * same value `refCore` observes for the `null` binding run_behavior is given. Lives ONLY in the observe
2266
- * companion. */
2746
+ * typed read; Value-typed → the raw Value clone). A missing key leaves Default (every covered input is
2747
+ * required + supplied by the corpus). Lives ONLY in the observe companion. */
2267
2748
  function emitInDecoder(comp, plan) {
2268
2749
  const name = inStructName(comp.name);
2269
2750
  const ports = Object.keys(comp.inputPorts ?? {});
@@ -2285,15 +2766,7 @@ function emitInDecoder(comp, plan) {
2285
2766
  const et = schema.elemType;
2286
2767
  const head = i === 0 ? " if" : " } else if";
2287
2768
  lines.push(`${head} k == ${rustStrLit(p)} {`);
2288
- if (inputPortIsOptional(schema)) {
2289
- // An OPTIONAL port decodes into its native Option<T>. materializeExpr's opt de-box IS the wire
2290
- // rule: `Value::Null` → None (absent), any other value → Some(<inner>). A key that never appears
2291
- // keeps the struct Default (None) — the same absent value. Checked FIRST: optionality wraps every
2292
- // inner type (an optional array is Option<Vec<T>>, not Vec<T>).
2293
- const optRef = inputPortTypeRef(schema, plan);
2294
- lines.push(` in_.${field} = ${materializeExpr("v", optRef, plan)};`);
2295
- }
2296
- else if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
2769
+ if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
2297
2770
  // #108: decode the boxed Value::Arr into the native Vec<ElemT> (test glue — materialize each elem).
2298
2771
  const elemRef = deriveTypeRef(et, plan);
2299
2772
  const elemTy = renderTypeRef(elemRef);
@@ -2313,6 +2786,11 @@ function emitInDecoder(comp, plan) {
2313
2786
  const mapRef = { kind: "map", value: deriveTypeRef(et, plan) };
2314
2787
  lines.push(` in_.${field} = ${materializeExpr("v", mapRef, plan)};`);
2315
2788
  }
2789
+ else if (inputPortIsOptional(schema)) {
2790
+ // #139: an OPTIONAL scalar port decodes into Option<T> via the inputPortTypeRef SSoT (a present key
2791
+ // materializes Some(v); an absent key never enters this block, leaving the Default None).
2792
+ lines.push(` in_.${field} = ${materializeExpr("v", inputPortTypeRef(schema, plan), plan)};`);
2793
+ }
2316
2794
  else if (scalar === undefined) {
2317
2795
  lines.push(` in_.${field} = v.clone();`);
2318
2796
  }
@@ -2331,39 +2809,6 @@ function emitInDecoder(comp, plan) {
2331
2809
  lines.push(`}`);
2332
2810
  return lines.join("\n");
2333
2811
  }
2334
- /** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
2335
- * declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row (a scalar/arr/
2336
- * opt row via its `val`; a NAMED row via its flattened fields copied from the struct port). A port with a
2337
- * different type is not echoable (the row could not hold it). */
2338
- function echoablePorts(comp, node, ref, plan, typedNodes) {
2339
- const want = renderTypeRef(ref);
2340
- return Object.keys(node.ports).filter((p) => portFieldRustType(node.ports[p], comp, plan, typedNodes, `port '${p}'`) === want);
2341
- }
2342
- /** emitDecodeRow — emit (once, deduped) a decoder `decode_row_<comp>_<node><suffix>(v: &Value) -> <rowT>`
2343
- * that materializes the scripted ok Value into the concrete row struct. Named: reuse the Value->struct
2344
- * marshaller + move fields; scalar/arr/opt: materialize into .val. TEST glue only. */
2345
- function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix) {
2346
- const fn = `decode_row_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}${suffix.toLowerCase()}`;
2347
- if (emitted.has(fn))
2348
- return fn;
2349
- emitted.add(fn);
2350
- const lines = [];
2351
- lines.push(`fn ${fn}(v: &Value) -> ${rowT} {`);
2352
- if (ref.kind === "named") {
2353
- const decl = findDecl(plan, ref.name);
2354
- lines.push(` let t = marshal_${ref.name}(v).expect("scripted row decode");`);
2355
- const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: t.${rustFieldName(f.name)}`).join(", ");
2356
- lines.push(` ${rowT} { ${inits}, ..Default::default() }`);
2357
- }
2358
- else {
2359
- const mat = materializeExpr("v", ref, plan);
2360
- lines.push(` let val = (|| -> Result<${renderTypeRef(ref)}, BehaviorError> { Ok(${mat}) })().expect("scripted row decode");`);
2361
- lines.push(` ${rowT} { val, ..Default::default() }`);
2362
- }
2363
- lines.push(`}`);
2364
- out.push(lines.join("\n"));
2365
- return fn;
2366
- }
2367
2812
  export const rustTypedNativeEmitter = {
2368
2813
  language: "rust-typed-native",
2369
2814
  classification: "codegen", // native: 型付きフラットコード生成・IR/dict 非在(#128/A6)