behavior-contracts 0.8.3 → 0.8.5

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 (36) hide show
  1. package/dist/authoring.d.ts +1 -1
  2. package/dist/authoring.d.ts.map +1 -1
  3. package/dist/authoring.js +41 -2
  4. package/dist/authoring.js.map +1 -1
  5. package/dist/behavior.d.ts +0 -24
  6. package/dist/behavior.d.ts.map +1 -1
  7. package/dist/behavior.js +24 -2
  8. package/dist/behavior.js.map +1 -1
  9. package/dist/generator/emit-shared-go-typed.d.ts.map +1 -1
  10. package/dist/generator/emit-shared-go-typed.js +7 -7
  11. package/dist/generator/emit-shared-go-typed.js.map +1 -1
  12. package/dist/generator/emit-shared-rust-typed.d.ts +8 -0
  13. package/dist/generator/emit-shared-rust-typed.d.ts.map +1 -1
  14. package/dist/generator/emit-shared-rust-typed.js +20 -6
  15. package/dist/generator/emit-shared-rust-typed.js.map +1 -1
  16. package/dist/generator/emit-shared-typescript.d.ts.map +1 -1
  17. package/dist/generator/emit-shared-typescript.js +18 -2
  18. package/dist/generator/emit-shared-typescript.js.map +1 -1
  19. package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
  20. package/dist/generator/emit-typed-native-go.js +231 -606
  21. package/dist/generator/emit-typed-native-go.js.map +1 -1
  22. package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
  23. package/dist/generator/emit-typed-native-rust.js +287 -648
  24. package/dist/generator/emit-typed-native-rust.js.map +1 -1
  25. package/dist/generator/native-expr.d.ts +91 -7
  26. package/dist/generator/native-expr.d.ts.map +1 -1
  27. package/dist/generator/native-expr.js +124 -19
  28. package/dist/generator/native-expr.js.map +1 -1
  29. package/dist/generator/typed.d.ts +26 -0
  30. package/dist/generator/typed.d.ts.map +1 -1
  31. package/dist/generator/typed.js +52 -0
  32. package/dist/generator/typed.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/package.json +2 -2
@@ -2,12 +2,12 @@ import { GeneratorFailure } from "./core.js";
2
2
  import { isControlGate } from "../behavior.js";
3
3
  import { rustStraightlineInternals } from "./emit-shared-rust.js";
4
4
  import { rustTypedInternals } from "./emit-shared-rust-typed.js";
5
- import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
5
+ import { buildTypePlan, deriveTypeRef, findDecl, inputPortIsOptional, inputPortTypeRef } from "./typed.js";
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
9
  const { sanitize, rustStrLit } = rustStraightlineInternals;
10
- const { rustFieldName, renderTypeRef, typedCell, typedFieldAccess, serializeTyped, emitTypeDecls, emitDefaults, emitSerializers, emitTypedValue, emitMarshallers, emitMarshalHelpers, materializeExpr, } = rustTypedInternals;
10
+ const { typeRefIsCopy, rustFieldName, renderTypeRef, typedCell, typedFieldAccess, serializeTyped, emitTypeDecls, emitDefaults, emitSerializers, emitTypedValue, emitMarshallers, emitMarshalHelpers, materializeExpr, } = rustTypedInternals;
11
11
  /** rustErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
12
12
  * plane (runtime-free). The covered module declares a local `BehaviorError` (see emitBehaviorErrorType);
13
13
  * the runner returns `Result<_, BehaviorError>` over THAT local type. This REPLACES the old
@@ -88,188 +88,15 @@ function staticArrElems(node) {
88
88
  return Array.isArray(arg) ? arg : null;
89
89
  }
90
90
  /**
91
- * portArrayElemRef (#110) resolve a componentRef input port that binds a RUNTIME ARRAY (an IN-list /
92
- * array-bound WHERE head) to its native element TypeRef, or null when not a covered array. The covered
93
- * shape is a SINGLE-SEGMENT, non-opt `ref` into an INPUT ARRAY port carrying a resolvable `elemType`
94
- * (PortSchema.elemType BC does NOT infer, consumer-interface C3). The port lowers to a native
95
- * `Vec<ElemT>` fed from `in_.<field>.clone()`. Reuses the bc#108 elemType lowering, extended from the
96
- * `map…over` element-source path to a plain componentRef port. A static string array is a different
97
- * covered shape (Vec<&'static str>) — handled ahead of this. No resolvable elemType / opt / multi-segment
98
- * ref / non-array schema / no plan → null (caller keeps its existing fail-closed behaviour). */
99
- function portArrayElemRef(node, inputPorts, plan) {
100
- if (plan === undefined)
101
- return null;
102
- if (staticStringArrElems(node) !== null)
103
- return null;
104
- const e = classifyExpr(node);
105
- if (e.kind !== "ref" || e.opt || e.path.length !== 1)
106
- return null;
107
- const schema = inputPorts?.[e.path[0]];
108
- if (schema === undefined || (schema.type !== "array" && schema.type !== "arr"))
109
- return null;
110
- const et = schema.elemType;
111
- if (et === undefined)
112
- return null;
113
- return deriveTypeRef(et, plan);
114
- }
115
- function inferPortType(node, inputPorts, asBinding) {
116
- if (staticArrElems(node) !== null)
117
- return "Value";
118
- const e = classifyExpr(node);
119
- switch (e.kind) {
120
- case "str":
121
- case "concat":
122
- return "String";
123
- case "bool":
124
- return "bool";
125
- case "ref": {
126
- // #108: a `$as`-headed ref (map element binding) — resolve the element field's scalar type.
127
- if (asBinding !== undefined && e.path[0] === asBinding.name) {
128
- try {
129
- const { ref } = typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan);
130
- if (ref.kind === "scalar" && ref.scalar !== "null")
131
- return rustScalar(ref.scalar);
132
- }
133
- catch {
134
- return "Value";
135
- }
136
- return "Value";
137
- }
138
- if (e.path.length === 1) {
139
- const s = inputPorts[e.path[0]];
140
- if (s) {
141
- if (s.type === "string" || s.type === "literal")
142
- return "String"; // literal = constrained String.
143
- if (s.type === "int")
144
- return "i64";
145
- if (s.type === "float" || s.type === "number")
146
- return "f64"; // `"number"` = the float channel.
147
- if (s.type === "bool")
148
- return "bool";
149
- }
150
- }
151
- return "Value";
152
- }
153
- default:
154
- return "Value";
155
- }
156
- }
157
- /** A static string-array port (every element a static string literal) — the graphddb `projection`
158
- * shape. Returns the list of literal strings, or null when the node is not that shape. */
159
- function staticStringArrElems(node) {
160
- const arr = staticArrElems(node);
161
- if (arr === null)
162
- return null;
163
- const out = [];
164
- for (const el of arr) {
165
- const e = classifyExpr(el);
166
- if (e.kind !== "str")
167
- return null;
168
- out.push(e.value);
169
- }
170
- return out;
171
- }
172
- /**
173
- * portFieldRustType — the NATIVE Rust type for a covered port field (bc#90 / runtime-free). The covered
174
- * ports struct carries NO `Value` field: every port lowers to a concrete Rust type.
175
- * - a static string-array (projection) → `Vec<&'static str>` (change #2);
176
- * - a bare int/float number literal (e.g. Query `limit: 20`) → `i64` / `f64`;
177
- * - a string/bool/scalar-input port → the scalar Rust type (inferPortType).
178
- * A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
179
- * Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no `Value`
180
- * fallback on the native module). `where` names the emission site for the error.
91
+ * portFieldRustTypethe 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.
181
96
  */
182
- function portFieldRustType(node, inputPorts, where = "port", asBinding, plan, typedNodes) {
183
- if (staticStringArrElems(node) !== null)
184
- return "Vec<&'static str>";
185
- // #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → Vec<ElemT>.
186
- const arrElem = portArrayElemRef(node, inputPorts, plan);
187
- if (arrElem !== null)
188
- return `Vec<${renderTypeRef(arrElem)}>`;
189
- // #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) → Vec<ElemT>.
190
- const priorArr = priorNodeArrElemInfo(node, typedNodes, plan);
191
- if (priorArr !== null)
192
- return `Vec<${renderTypeRef(priorArr.elemRef)}>`;
193
- const num = numLiteralRustExpr(node);
194
- if (num !== null)
195
- return typeof node === "number" && Number.isInteger(node) ? "i64" : "f64";
196
- // a ref to a `$as` element field (or an input port) whose type is a MAP or NAMED struct (NOT a bare
197
- // scalar) → the native map/struct type. #108-map: a `{map:V}`-typed field (e.g. a nested
198
- // `map<map<obj>>` write port) lowers to BTreeMap; a named field lowers to its struct. Reuses the same
199
- // typedFieldAccess resolution as the scalar path; a genuinely-untyped ref still falls through.
200
- const composite = portCompositeRef(node, inputPorts, asBinding, plan);
201
- if (composite !== null)
202
- return renderTypeRef(composite);
203
- const ft = inferPortType(node, inputPorts, asBinding);
204
- if (ft === "Value") {
205
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90 / runtime-free): ${where} '${JSON.stringify(node)}' does not lower to a native Rust type (would need a boxed Value). The covered read plane is runtime-free and admits NO boxed escape — the covered corpus ports are string / bool / static-string-array (projection) / bare-number (limit) only. Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
206
- }
207
- return ft;
208
- }
209
- /** portCompositeRef — the full TypeRef of a port that is a `{ref:[...]}` into a `$as` element field OR
210
- * an input port whose type is a MAP or NAMED struct (a non-scalar covered field). Returns null when the
211
- * node is not such a ref (so the caller falls through to the scalar/array inference). #108-map: enables a
212
- * `{map:V}`-typed field port (e.g. a nested map write port) to lower to the native BTreeMap. */
213
- function portCompositeRef(node, inputPorts, asBinding, plan) {
214
- const e = classifyExpr(node);
215
- if (e.kind !== "ref" || plan === undefined)
216
- return null;
217
- let ref = null;
218
- if (asBinding !== undefined && e.path[0] === asBinding.name) {
219
- try {
220
- ref = typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan).ref;
221
- }
222
- catch {
223
- return null;
224
- }
225
- }
226
- else if (e.path.length >= 1) {
227
- const s = inputPorts[e.path[0]];
228
- // a whole-port ref to an input MAP port carrying an elemType → its BTreeMap type (mirrors
229
- // inputPortRustType); a deeper path into an input port is not resolved here (no field type source).
230
- if (s !== undefined && s.type === "map" && e.path.length === 1) {
231
- const et = s.elemType;
232
- if (et !== undefined)
233
- return { kind: "map", value: deriveTypeRef(et, plan) };
234
- }
235
- return null;
236
- }
237
- if (ref === null)
238
- return null;
239
- // only MAP / NAMED composite fields here — scalars/arrays keep their existing dedicated paths.
240
- if (ref.kind === "map" || ref.kind === "named")
241
- return ref;
242
- return null;
243
- }
244
- /** emitCompositePortValue — the OWNED native value expr for a port that is a ref to a `$as` element
245
- * MAP/NAMED field (or an input map port): a typed field access `.clone()` (the concrete BTreeMap/struct,
246
- * ZERO boxed Value). `elemBaseExpr` is the map arm's element base (`oel_<mapId>`) for a `$as`-headed ref;
247
- * omitted at the top level (only input-port composite refs resolve there). Returns null otherwise. */
248
- function emitCompositePortValue(node, inputPorts, asBinding, plan, elemBaseExpr) {
249
- const e = classifyExpr(node);
250
- if (e.kind !== "ref")
251
- return null;
252
- if (asBinding !== undefined && elemBaseExpr !== undefined && e.path[0] === asBinding.name) {
253
- let acc;
254
- try {
255
- acc = typedFieldAccess(elemBaseExpr, asBinding.ref, e.path.slice(1), plan);
256
- }
257
- catch {
258
- return null;
259
- }
260
- if (acc.ref.kind !== "map" && acc.ref.kind !== "named")
261
- return null;
262
- return `${acc.expr}.clone()`;
263
- }
264
- // a WHOLE-port ref to an input MAP port (with elemType) — valid at top level OR inside a map node (a
265
- // ref head that is NOT the `$as` element binding). Clones the native BTreeMap off the input struct.
266
- if ((asBinding === undefined || e.path[0] !== asBinding.name) && e.path.length === 1) {
267
- const s = inputPorts[e.path[0]];
268
- if (s !== undefined && s.type === "map" && s.elemType !== undefined) {
269
- return `in_.${rustFieldName(e.path[0])}.clone()`;
270
- }
271
- }
272
- return null;
97
+ function portFieldRustType(node, comp, plan, typedNodes, where = "port", opts) {
98
+ const c = compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), where, opts);
99
+ return c.renderedType ?? renderTypeRef(c.ref);
273
100
  }
274
101
  /**
275
102
  * emitPortsStruct — the CONCRETE native ports struct for a componentRef node. Typed fields per the
@@ -278,13 +105,13 @@ function emitCompositePortValue(node, inputPorts, asBinding, plan, elemBaseExpr)
278
105
  * reads `ports.f_<field>` DIRECTLY. The struct derives Clone (a bc#87 parallel stage clones it into the
279
106
  * per-worker dispatch). Every field is a NATIVE Rust type (bc#90 / runtime-free) — NO boxed `Value`.
280
107
  */
281
- function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
108
+ function emitPortsStruct(comp, node, plan, typedNodes, asBinding) {
282
109
  const name = portsStructName(comp.name, node.id);
283
110
  const portNames = Object.keys(node.ports);
284
111
  if (portNames.length === 0) {
285
112
  return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
286
113
  }
287
- const types = portNames.map((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding, plan, typedNodes));
114
+ 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));
288
115
  const fields = portNames
289
116
  .map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
290
117
  .join("\n");
@@ -308,7 +135,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
308
135
  // #108: element ports may read the `$as` binding — resolve the over element type for native typing.
309
136
  const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
310
137
  const asBinding = { name: m.as, ref: elemRef, plan };
311
- const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan);
138
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, plan, typedNodes, asBinding);
312
139
  return `${elemStruct}
313
140
 
314
141
  // ${batch} — CONCRETE batched ports for map '${node.id}': the Vec of per-element CONCRETE ports structs
@@ -326,7 +153,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
326
153
  const batch = `${name}Batch`;
327
154
  const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
328
155
  const asBinding = { name: f.as, ref: elemRef, plan };
329
- const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan);
156
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, plan, typedNodes, asBinding);
330
157
  return `${elemStruct}
331
158
 
332
159
  // ${batch} — CONCRETE batched ports for fanout '${node.id}': the Vec of per-id CONCRETE ports structs.
@@ -389,25 +216,47 @@ function makeRustExprBackend(resolveHead, plan) {
389
216
  notOp: (a) => `(!${a})`,
390
217
  structLit: (name, inits) => `${name} { ${inits.map((i) => `${rustFieldName(i.field)}: ${i.expr}`).join(", ")} }`,
391
218
  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).
221
+ staticStrArr: (literals) => ({ expr: `vec![${literals.map((s) => rustStrLit(s)).join(", ")}]`, type: "Vec<&'static str>" }),
392
222
  // an opt (`Option<T>`) NONE literal. Annotate as `Option::<T>::None` so it type-infers in ANY
393
223
  // position (a bare `None` can be ambiguous; the explicit turbofish keeps it a fully-typed value).
394
224
  optNone: (innerTy) => `Option::<${innerTy}>::None`,
395
225
  // opt (`Option<T>`) null-presence test: present = `expr.is_some()`, absent = `expr.is_none()`.
396
- optIsSome: (expr) => `${expr}.is_some()`,
397
- optIsNone: (expr) => `${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.
237
+ optUnwrapOr: (optExpr, defaultExpr, _innerTy) => /^[A-Za-z0-9_.:]+$/.test(defaultExpr) ? `${optExpr}.unwrap_or(${defaultExpr})` : `${optExpr}.unwrap_or_else(|| ${defaultExpr})`,
238
+ // 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.
241
+ optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
242
+ `let ${temp}: ${ty} = match ${optExpr} {`,
243
+ ` Some(${bindTemp}) => ${bindTemp},`,
244
+ ` None => { ${rustBlockBody(dStmts, dExpr)} }`,
245
+ `};`,
246
+ ],
398
247
  ternary: (cond, t, e, _ty) => `(if ${rustStripOuterParens(cond)} { ${rustStripOuterParens(t)} } else { ${rustStripOuterParens(e)} })`,
399
248
  shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
400
249
  // and: let temp = if left { <rStmts> rExpr } else { false }; or: if left { true } else { <rStmts> rExpr }.
401
250
  // build a block that runs rStmts then yields rExpr (parens stripped — block-tail is not a sub-expr).
402
- const rblock = `{ ${rStmts.join(" ")} ${rustStripOuterParens(rExpr)} }`;
251
+ const rblock = `{ ${rustBlockBody(rStmts, rExpr)} }`;
403
252
  if (op === "and") {
404
253
  return [`let ${temp}: bool = if ${rustStripOuterParens(left)} ${rblock} else { false };`];
405
254
  }
406
255
  return [`let ${temp}: bool = if ${rustStripOuterParens(left)} { true } else ${rblock};`];
407
256
  },
408
257
  condGuard: (temp, ty, cond, tStmts, tExpr, eStmts, eExpr) => {
409
- const tblock = `{ ${tStmts.join(" ")} ${rustStripOuterParens(tExpr)} }`;
410
- const eblock = `{ ${eStmts.join(" ")} ${rustStripOuterParens(eExpr)} }`;
258
+ const tblock = `{ ${rustBlockBody(tStmts, tExpr)} }`;
259
+ const eblock = `{ ${rustBlockBody(eStmts, eExpr)} }`;
411
260
  return [`let ${temp}: ${ty} = if ${rustStripOuterParens(cond)} ${tblock} else ${eblock};`];
412
261
  },
413
262
  };
@@ -416,6 +265,17 @@ function makeRustExprBackend(resolveHead, plan) {
416
265
  function rustSnake(name) {
417
266
  return name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").replace(/([0-9])([a-z])/g, "$1_$2").toLowerCase();
418
267
  }
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
+ }
419
279
  /** strip ONE redundant outer paren pair from an expr (clippy `if (cond)` unused_parens). Only when the
420
280
  * whole string is a single balanced `( … )` — a `(a) && (b)` stays untouched. */
421
281
  function rustStripOuterParens(expr) {
@@ -433,6 +293,12 @@ function rustStripOuterParens(expr) {
433
293
  }
434
294
  return expr.slice(1, -1);
435
295
  }
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
+ }
436
302
  /** Rust float literal (finite; integral gets `.0`). */
437
303
  function rustFloatLitExpr(n) {
438
304
  if (Number.isInteger(n))
@@ -781,44 +647,6 @@ function mapOverElemInfo(comp, node, typedNodes, plan) {
781
647
  function mapOverElemType(comp, node, typedNodes, plan) {
782
648
  return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
783
649
  }
784
- /**
785
- * priorNodeArrElemInfo (#129, rust twin) — resolve a leaf port that is a `ref` into a PRIOR BODY NODE
786
- * whose annotated `outType` is (or reaches, through a field path) a native array, to its element TypeRef
787
- * + the OWNED Rust Vec expression that yields it. GENERALIZES the `map…over` prior-node-arr dataflow
788
- * (mapOverElemInfo's first branch) to ANY leaf input port: a fold/dedup leaf whose `items` port is
789
- * `{ref:["<priorNode>"]}` (or `.items` of a prior connection node) now lowers to `Vec<ElemT>` cloned
790
- * out of the prior node's typed cell — ZERO boxed Value on the read hot path.
791
- *
792
- * The element type comes STRICTLY from the prior node's EXPLICIT `outType` annotation walked through
793
- * typedFieldAccess (BC does NOT infer — consumer-interface C3 / [[graphddb-typed-native-static-link]]):
794
- * if the head is not a prior body node, or its outType (through the field path) is not an `arr`, this
795
- * returns null and the caller keeps its existing fail-closed behaviour (never a silent box). A
796
- * single-segment INPUT-array port (elemType) is a DIFFERENT covered shape (portArrayElemRef) — handled
797
- * ahead of this. Requires the typedNodes map (prior-node outType TypeRefs) and a TypePlan.
798
- */
799
- function priorNodeArrElemInfo(node, typedNodes, plan) {
800
- if (typedNodes === undefined || plan === undefined)
801
- return null;
802
- if (staticArrElems(node) !== null)
803
- return null; // a static array literal — not a prior-node ref.
804
- const e = classifyExpr(node);
805
- if (e.kind !== "ref" || e.opt || e.path.length < 1)
806
- return null;
807
- if (!typedNodes.has(e.path[0]))
808
- return null; // head is not a prior body node → not this shape.
809
- const baseRef = typedNodes.get(e.path[0]);
810
- let acc;
811
- try {
812
- acc = typedFieldAccess(`${typedCell(e.path[0])}.borrow()`, baseRef, e.path.slice(1), plan);
813
- }
814
- catch {
815
- return null; // the field path does not resolve on the prior node's outType → fail-closed at caller.
816
- }
817
- if (acc.ref.kind !== "arr")
818
- return null; // the prior-node result (through the path) is not an array.
819
- // OWNED: clone the borrowed slice out of the RefCell so the ports struct owns its Vec<ElemT>.
820
- return { elemRef: acc.ref.elem, overExpr: `${acc.expr}.clone()` };
821
- }
822
650
  // ── eligibility ─────────────────────────────────────────────────────────────────────
823
651
  function reconstructR(e) {
824
652
  switch (e.kind) {
@@ -836,50 +664,35 @@ function reconstructR(e) {
836
664
  return e.node;
837
665
  }
838
666
  }
839
- /**
840
- * numLiteralRustExpr a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
841
- * `classifyExpr` leaves a bare number `dynamic` (its checked int/float range rules are the evaluate
842
- * SSoT), so it never reaches the static str/bool/null branch — but a number literal IS statically
843
- * resolvable to a native `Value`: the SAME range check the runtime applies (integral literal in the
844
- * safe 2^53-1 window Value::Int; a fractional/exponent literal the checked Value::Float) is
845
- * performed HERE at generation time. Returns the Rust expression for the literal's Value, or null
846
- * when the node is not a bare number OR an integral literal is out of the safe window (keeping the
847
- * runtime INVALID_LITERAL semantics — the port is not covered natively, the component falls through).
848
- * Mirrors behavior_contracts eval of a bare number literal exactly.
849
- */
850
- const NUM_SAFE_INT_R = 9007199254740991; // 2^53 - 1 (expr safeInt)
851
- function numLiteralRustExpr(node) {
852
- if (typeof node !== "number" || !Number.isFinite(node))
853
- return null;
854
- if (Number.isInteger(node)) {
855
- if (node < -NUM_SAFE_INT_R || node > NUM_SAFE_INT_R)
856
- return null; // out of safe window → not covered
857
- return `Value::Int(${node}i64)`;
858
- }
859
- // fractional / exponent literal → checked f64 (finite, guaranteed above).
860
- return `Value::Float(${node}f64)`;
861
- }
862
- /** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
863
- * resolvable to a native Value with no Obj / no evaluate interpreter. */
864
- function portIsStatic(node) {
865
- const arr = staticArrElems(node);
866
- if (arr !== null)
867
- return arr.every(portIsStatic);
868
- if (numLiteralRustExpr(node) !== null)
869
- return true; // bare number literal (e.g. Query `limit`)
870
- const e = classifyExpr(node);
871
- switch (e.kind) {
872
- case "str":
873
- case "bool":
874
- case "null":
875
- case "ref":
876
- return true;
877
- case "concat":
878
- return e.parts.every((p) => portIsStatic(reconstructR(p)));
879
- default:
880
- return false;
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.
881
677
  }
882
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. */
681
+ function buildTypedNodes(comp, plan) {
682
+ const typedNodes = new Map();
683
+ for (const n of comp.body) {
684
+ if (isControlGate(n))
685
+ continue;
686
+ // coverage probes under-typed components too; an unresolvable node just isn't a prior-node cell.
687
+ try {
688
+ typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
689
+ }
690
+ catch {
691
+ /* untyped node — omit */
692
+ }
693
+ }
694
+ return typedNodes;
695
+ }
883
696
  /** The output must lower to a typed struct/value with no scope read: a ref to a BODY NODE, an
884
697
  * obj/arr of such, or a concat. A ref to an input param / non-concat operator is not lowerable. */
885
698
  function outputIsNativeLowerable(comp) {
@@ -911,7 +724,7 @@ function outputIsNativeLowerable(comp) {
911
724
  /**
912
725
  * coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet), rust twin of the go predicate.
913
726
  */
914
- function coveredMapNode(n, bodyIds, comp) {
727
+ function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
915
728
  if (n === null || typeof n !== "object" || !("map" in n))
916
729
  return false;
917
730
  const m = n.map;
@@ -943,14 +756,25 @@ function coveredMapNode(n, bodyIds, comp) {
943
756
  return false;
944
757
  }
945
758
  const ports = m.ports;
759
+ const asBinding = mapAsBinding(comp, n, typedNodes, plan);
946
760
  for (const p of Object.keys(ports))
947
- if (!portIsStatic(ports[p]))
761
+ if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
948
762
  return false;
949
763
  return true;
950
764
  }
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
+ }
951
775
  /** coveredFanoutNode (v3, rust twin) — a first-class fanout node is covered when its over is a covered
952
776
  * ref (prior-node arr / input array with elemType) and its element ports are static. See the go twin. */
953
- function coveredFanoutNode(n, bodyIds, comp) {
777
+ function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
954
778
  if (n === null || typeof n !== "object" || !("fanout" in n))
955
779
  return false;
956
780
  const f = n.fanout;
@@ -970,8 +794,15 @@ function coveredFanoutNode(n, bodyIds, comp) {
970
794
  return false;
971
795
  }
972
796
  const ports = f.ports;
797
+ let asBinding;
798
+ try {
799
+ asBinding = { name: f.as, ref: fanoutOverElemInfo(comp, n, typedNodes, plan).elemRef, plan };
800
+ }
801
+ catch {
802
+ asBinding = undefined;
803
+ }
973
804
  for (const p of Object.keys(ports))
974
- if (!portIsStatic(ports[p]))
805
+ if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
975
806
  return false;
976
807
  return true;
977
808
  }
@@ -1007,8 +838,8 @@ function fanoutOverElemInfo(comp, node, typedNodes, plan) {
1007
838
  }
1008
839
  /** A covered read SHAPE with static ports + native-lowerable output (+ covered map...into), OR a
1009
840
  * real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point reads. */
1010
- function isSequentialComponentRefRead(comp) {
1011
- return coverageRejectReason(comp) === null;
841
+ function isSequentialComponentRefRead(comp, plan) {
842
+ return coverageRejectReason(comp, plan) === null;
1012
843
  }
1013
844
  /**
1014
845
  * coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89), rust twin of
@@ -1017,10 +848,11 @@ function isSequentialComponentRefRead(comp) {
1017
848
  * IDENTICAL to isSequentialComponentRefRead (defined as `coverageRejectReason(c) === null`) so the two
1018
849
  * can never disagree. This is the message the fail-closed dispatch surfaces per non-native component.
1019
850
  */
1020
- function coverageRejectReason(comp) {
851
+ function coverageRejectReason(comp, plan) {
1021
852
  const ep = execPlan(comp);
1022
853
  if (ep === null)
1023
854
  return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
855
+ const typedNodes = buildTypedNodes(comp, plan);
1024
856
  // #114: a bindField relation child (single|connection) IS covered inside a parallel stage — the
1025
857
  // sibling hasMany fan-out (parent → members ∥ permissions). Its bound-key skip-gate is preflighted
1026
858
  // per member (from the settled parent) BEFORE dispatch, so the scoped-thread fan-out keeps
@@ -1058,12 +890,12 @@ function coverageRejectReason(comp) {
1058
890
  continue;
1059
891
  }
1060
892
  if ("fanout" in n) {
1061
- if (!coveredFanoutNode(n, bodyIds, comp))
893
+ if (!coveredFanoutNode(n, bodyIds, comp, plan, typedNodes))
1062
894
  return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
1063
895
  continue;
1064
896
  }
1065
897
  if ("map" in n) {
1066
- if (!coveredMapNode(n, bodyIds, comp))
898
+ if (!coveredMapNode(n, bodyIds, comp, plan, typedNodes))
1067
899
  return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
1068
900
  continue;
1069
901
  }
@@ -1082,7 +914,7 @@ function coverageRejectReason(comp) {
1082
914
  if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
1083
915
  return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
1084
916
  for (const p of Object.keys(cr.ports))
1085
- if (portIsStatic(cr.ports[p]) === false)
917
+ if (portIsStatic(cr.ports[p], comp, plan, typedNodes) === false)
1086
918
  return `node '${n.id}' port '${p}' is not statically resolvable`;
1087
919
  }
1088
920
  if (!outputIsNativeLowerable(comp))
@@ -1099,55 +931,42 @@ function isFullyTyped(comp) {
1099
931
  return false;
1100
932
  return true;
1101
933
  }
1102
- function isNativeRaw(comp) {
1103
- return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
934
+ function isNativeRaw(comp, plan) {
935
+ return isSequentialComponentRefRead(comp, plan) && isFullyTyped(comp);
1104
936
  }
1105
- function assertTypedCoverage(comp) {
1106
- if (!isSequentialComponentRefRead(comp))
937
+ function assertTypedCoverage(comp, plan) {
938
+ if (!isSequentialComponentRefRead(comp, plan))
1107
939
  return;
1108
940
  if (isFullyTyped(comp))
1109
941
  return;
1110
942
  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).`);
1111
943
  }
1112
944
  // ── typed input struct (InNR<Comp>) + NATIVE scalar port lowering ────────────────────────────
1113
- /** Rust type for an input port's declared type (scalar → concrete; array+elemType → Vec<ElemT>; else Value). */
1114
- function inputPortRustType(schema, plan) {
1115
- switch (schema?.type) {
1116
- case "string":
1117
- // a `"literal"` port is a constrained STRING union (`param.literal('a','b',…)`, stored 'S') its
1118
- // native representation is a plain String (the union constraint is a validation concern, not a type).
1119
- case "literal":
1120
- return "String";
1121
- case "int":
1122
- return "i64";
1123
- case "float":
1124
- // a `"number"` port is a FLOAT (run_behavior: `typeName(number) === "float"`, expr-eval.ts §value):
1125
- // a JS number is IEEE754 → f64. (An i64 value is declared `"int"`; `"number"` is the float channel.)
1126
- case "number":
1127
- return "f64";
1128
- case "bool":
1129
- return "bool";
1130
- case "array":
1131
- case "arr": {
1132
- // #108: an input ARRAY port with a declared elemType lowers to a native Vec<ElemT>.
1133
- const et = schema.elemType;
1134
- if (et !== undefined && plan !== undefined)
1135
- return `Vec<${renderTypeRef(deriveTypeRef(et, plan))}>`;
1136
- return "Value";
1137
- }
1138
- case "map": {
1139
- // an input MAP port (dynamic string keys, declared value elemType) lowers to a native BTreeMap.
1140
- const et = schema.elemType;
1141
- if (et !== undefined && plan !== undefined)
1142
- return `std::collections::BTreeMap<String, ${renderTypeRef(deriveTypeRef(et, plan))}>`;
1143
- return "Value";
1144
- }
1145
- default:
1146
- return "Value";
1147
- }
1148
- }
1149
- /** The scalar kind an input port declares, or undefined if it is not a native scalar. */
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
+ */
955
+ function inputPortRustType(schema, plan, where = "input port") {
956
+ const ref = inputPortTypeRef(schema, plan);
957
+ if (ref !== undefined)
958
+ return renderTypeRef(ref);
959
+ 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.`);
961
+ return "Value";
962
+ }
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`. */
1150
967
  function inputScalarKind(schema) {
968
+ if (inputPortIsOptional(schema))
969
+ return undefined;
1151
970
  switch (schema?.type) {
1152
971
  case "string":
1153
972
  case "literal": // a `"literal"` union is a constrained String (see inputPortRustType).
@@ -1172,7 +991,7 @@ function emitInStruct(comp, plan) {
1172
991
  return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\n#[derive(Default)]\npub struct ${name};`;
1173
992
  }
1174
993
  const fields = ports
1175
- .map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p], plan)}, // ${JSON.stringify(p)}`)
994
+ .map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`)}, // ${JSON.stringify(p)}`)
1176
995
  .join("\n");
1177
996
  return `// ${name} — the CONCRETE input for '${comp.name}' (fields = inputPorts; typed, consumer-built —
1178
997
  // NO generic Value slice, NO per-field boxing crosses the covered read boundary).
@@ -1181,166 +1000,21 @@ pub struct ${name} {
1181
1000
  ${fields}
1182
1001
  }`;
1183
1002
  }
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. */
1184
1005
  /**
1185
- * emitNativeScalarlower a static port expr to a NATIVE Rust expression of the concrete `want` scalar
1186
- * type, or null if it cannot be statically proven that type. A string literal is native for
1187
- * `want==="string"`, a bool literal for `want==="bool"`. A ref resolves via `resolveRef`. A concat lowers
1188
- * ONLY into `string` and only when EVERY part is a native string, emitting a single `format!("{}{}…", …)`.
1189
- */
1190
- function emitNativeScalar(node, want, resolveRef) {
1191
- if (staticArrElems(node) !== null)
1192
- return null;
1193
- const e = classifyExpr(node);
1194
- switch (e.kind) {
1195
- case "str":
1196
- return want === "String" ? `${rustStrLit(e.value)}.to_string()` : null;
1197
- case "bool":
1198
- return want === "bool" ? (e.value ? "true" : "false") : null;
1199
- case "null":
1200
- return null;
1201
- case "ref": {
1202
- const sc = resolveRef(e.path[0], e.path.slice(1), e.opt);
1203
- if (sc === null || sc.scalar !== want)
1204
- return null;
1205
- return sc.expr;
1206
- }
1207
- case "concat": {
1208
- if (want !== "String")
1209
- return null;
1210
- // a single-part concat is just the owned String of that part.
1211
- if (e.parts.length === 1)
1212
- return emitNativeScalar(reconstructR(e.parts[0]), "String", resolveRef);
1213
- // multi-part: fold into ONE format! (statically strings only — the concat's strings-only
1214
- // TYPE_MISMATCH can never fire, so dropping the check is sound). To stay clippy-clean
1215
- // (to_string_in_format_args), a string-literal part is baked into the format STRING itself
1216
- // (escaped) rather than passed as a `{}` arg; a ref part becomes a `{}` arg (Display on String).
1217
- let fmt = "";
1218
- const args = [];
1219
- for (const p of e.parts) {
1220
- const sub = classifyExpr(reconstructR(p));
1221
- if (sub.kind === "str") {
1222
- fmt += sub.value.replace(/[{}]/g, (c) => c + c); // escape { } for the format string
1223
- continue;
1224
- }
1225
- const arg = emitNativeScalar(reconstructR(p), "String", resolveRef);
1226
- if (arg === null)
1227
- return null;
1228
- fmt += "{}";
1229
- args.push(arg);
1230
- }
1231
- if (args.length === 0)
1232
- return `${rustStrLit(fmt)}.to_string()`;
1233
- return `format!(${rustStrLit(fmt)}, ${args.join(", ")})`;
1234
- }
1235
- default:
1236
- return null;
1237
- }
1238
- }
1239
- // bc#90 / runtime-free: the old `emitStaticR` / `inputLeafR` / `valueLeafR` port lowering (which built
1240
- // `Value` leaves via the primitives::* helpers and struct-field serializers) is REMOVED — every covered
1241
- // port now lowers to a FULLY NATIVE Rust value in emitPortInit (string / bool / static-string-array /
1242
- // number literal), and a genuinely-dynamic port fails closed. There is no boxed-Value fallback on the
1243
- // covered read plane.
1244
- /**
1245
- * emitPortInit — emit the port field build for one port as a FULLY NATIVE Rust expression (bc#90 /
1246
- * runtime-free). Every covered port lowers to a concrete Rust value assigned straight into the ports
1247
- * struct field — NO `Value`, NO fallback path, NO `match`:
1248
- * - a static string array (projection) → `vec!["a", "b", ...]` (change #2);
1249
- * - a bare int/float number literal (e.g. Query `limit: 20`) → `20i64` / `<n>f64`;
1250
- * - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
1251
- * A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered plane —
1252
- * a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
1253
- * ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
1006
+ * emitPortInitthe 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.
1254
1010
  */
1255
- function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan, typedNodes, asBinding, elemBaseExpr) {
1256
- const field = portFieldName(pn);
1257
- // #108-map: a port that is a ref to a `$as` element MAP/NAMED field (or an input map port) → CLONE the
1258
- // typed composite value directly off the element/input (native BTreeMap / struct — ZERO boxed Value).
1259
- // Reuses portCompositeRef for the type; the value is the typed field access `.clone()`.
1260
- if (plan !== undefined) {
1261
- const compExpr = emitCompositePortValue(expr, inputPorts ?? {}, asBinding, plan, elemBaseExpr);
1262
- if (compExpr !== null)
1263
- return `${field}: ${compExpr}`;
1264
- }
1265
- // #110: a componentRef port bound to a runtime array input port (with elemType) → the native Vec<ElemT>,
1266
- // cloned out of the concrete input struct (`in_.<field>.clone()`) — ZERO boxed Value on the read hot path.
1267
- if (inputPorts !== undefined && portArrayElemRef(expr, inputPorts, plan) !== null) {
1268
- const e = classifyExpr(expr);
1269
- if (e.kind === "ref" && e.path.length === 1) {
1270
- return `${field}: in_.${rustFieldName(e.path[0])}.clone()`;
1271
- }
1272
- }
1273
- // #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) →
1274
- // the native Vec<ElemT> cloned off the prior node's typed cell (`t_<node>.borrow().field….clone()`) —
1275
- // ZERO boxed Value on the read hot path (a fold/dedup leaf whose `items` = a prior node's array result).
1276
- const priorArr = priorNodeArrElemInfo(expr, typedNodes, plan);
1277
- if (priorArr !== null)
1278
- return `${field}: ${priorArr.overExpr}`;
1279
- // change #2: a static string array (projection) → a native `vec![...]` of &'static str.
1280
- const strArr = staticStringArrElems(expr);
1281
- if (strArr !== null) {
1282
- if (fieldRustType !== "Vec<&'static str>") {
1283
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90): port '${pn}' is a static string array but its field type is '${fieldRustType}' (expected Vec<&'static str>).`);
1284
- }
1285
- return `${field}: vec![${strArr.map((s) => rustStrLit(s)).join(", ")}]`;
1286
- }
1287
- // a bare number literal (e.g. Query `limit: 20`): the SSoT range check ran in numLiteralRustExpr —
1288
- // emit the concrete native i64/f64 (unwrap the Value::Int/Float box: it was only there for the old
1289
- // boxed path; the range check is what matters, and it already passed).
1290
- const num = numLiteralRustExpr(expr);
1291
- if (num !== null) {
1292
- if (fieldRustType === "i64")
1293
- return `${field}: ${expr}i64`;
1294
- if (fieldRustType === "f64")
1295
- return `${field}: ${expr}f64`;
1296
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90): port '${pn}' is a number literal but its field type is '${fieldRustType}'.`);
1297
- }
1298
- // string / bool / scalar-input / concat → native scalar expression (no Value, no `match`).
1299
- const native = emitNativeScalar(expr, fieldRustType, resolveRef);
1300
- if (native !== null) {
1301
- return `${field}: ${native}`;
1302
- }
1303
- // FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would re-couple
1304
- // the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the component is not
1305
- // native-eligible and must regenerate on the boxed straight-line path.
1306
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90 / runtime-free): port '${pn}' (${JSON.stringify(expr)}) does not lower to a native Rust value of type '${fieldRustType}'. The covered read plane admits NO boxed Value escape — regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
1011
+ function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
1012
+ const c = compilePortNode(expr, comp, plan, typedNodes, isPrior, `port '${pn}'`, opts);
1013
+ 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.`);
1015
+ return `${portFieldName(pn)}: ${c.expr}`;
1307
1016
  }
1308
1017
  // ── the combined runner (STRUCT-returning; INLINE sequential; native ports in; concrete row out) ──
1309
- /** resolveRefAt for the top-level runner: a ref head is a PRIOR NODE (its typed scalar field) or an
1310
- * INPUT PORT (in_.<field>). A REQUIRED scalar resolves to an OWNED native Rust expression; opt / non-
1311
- * scalar / refOpt → null (fall back to the Value path). */
1312
- function runnerResolveRef(priorNodeAt, atPos, typedNodes, plan, inputPorts) {
1313
- return (head, restPath, opt) => {
1314
- if (opt)
1315
- return null;
1316
- if (priorNodeAt(head, atPos)) {
1317
- const baseRef = typedNodes.get(head);
1318
- let acc;
1319
- let expr;
1320
- try {
1321
- const r = typedFieldAccess(`${typedCell(head)}.borrow()`, baseRef, restPath, plan);
1322
- expr = r.expr;
1323
- acc = r.ref;
1324
- }
1325
- catch {
1326
- return null;
1327
- }
1328
- if (acc.kind !== "scalar" || acc.scalar === "null")
1329
- return null;
1330
- // OWNED: a string field is cloned; int/float/bool are Copy (deref off the Ref).
1331
- const owned = acc.scalar === "string" ? `(${expr}).clone()` : `*(${expr})`;
1332
- return { expr: owned, scalar: rustScalar(acc.scalar) };
1333
- }
1334
- if (restPath.length !== 0)
1335
- return null;
1336
- const scalar = inputScalarKind(inputPorts?.[head]);
1337
- if (scalar === undefined)
1338
- return null;
1339
- const field = `in_.${rustFieldName(head)}`;
1340
- const owned = scalar === "String" ? `${field}.clone()` : field;
1341
- return { expr: owned, scalar };
1342
- };
1343
- }
1344
1018
  function rustScalar(s) {
1345
1019
  return s === "string" ? "String" : s === "int" ? "i64" : s === "float" ? "f64" : "bool";
1346
1020
  }
@@ -1532,12 +1206,9 @@ function emitNativeRawRunner(comp, plan, ap) {
1532
1206
  indent += " ";
1533
1207
  }
1534
1208
  }
1535
- const resolveRef = runnerResolveRef(priorNodeAt, k, typedNodes, plan, comp.inputPorts);
1536
1209
  const inits = [];
1537
1210
  for (const pn of portNames) {
1538
- const expr = node.ports[pn];
1539
- const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
1540
- inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan, typedNodes));
1211
+ inits.push(emitPortInit(pn, node.ports[pn], comp, plan, typedNodes, (h) => priorNodeAt(h, k)));
1541
1212
  }
1542
1213
  const boundArg = node.bindField !== undefined ? `bound_${s}` : "None";
1543
1214
  // bc#97: derived per-node await — this point-read's future is awaited HERE (its result-consumption
@@ -1665,12 +1336,9 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1665
1336
  ind += " ";
1666
1337
  }
1667
1338
  }
1668
- const resolveRef = runnerResolveRef(priorNodeAt, atPos, typedNodes, plan, comp.inputPorts);
1669
1339
  const inits = [];
1670
1340
  for (const pn of Object.keys(node.ports)) {
1671
- const expr = node.ports[pn];
1672
- const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
1673
- inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan, typedNodes));
1341
+ inits.push(emitPortInit(pn, node.ports[pn], comp, plan, typedNodes, (h) => priorNodeAt(h, atPos)));
1674
1342
  }
1675
1343
  if (gated) {
1676
1344
  lines.push(`${ind}ports_${s} = Some(${structName} { ${inits.join(", ")} });`);
@@ -1781,78 +1449,18 @@ isAsync = false) {
1781
1449
  const portNames = Object.keys(m.ports);
1782
1450
  const elemArrRef = typedNodes.get(node.id);
1783
1451
  const elemName = renderTypeRef(elemArrRef.elem);
1784
- const resolveRef = (head, restPath, opt) => {
1785
- if (opt)
1786
- return null;
1787
- let baseExpr;
1788
- let baseRef;
1789
- if (head === asName) {
1790
- baseExpr = `oel_${s}`;
1791
- baseRef = overElemRef;
1792
- }
1793
- else if (priorHere(head)) {
1794
- baseExpr = `${typedCell(head)}.borrow()`;
1795
- baseRef = typedNodes.get(head);
1796
- }
1797
- else {
1798
- if (restPath.length !== 0)
1799
- return null;
1800
- const scalar = inputScalarKind(comp.inputPorts?.[head]);
1801
- if (scalar === undefined)
1802
- return null;
1803
- const field = `in_.${rustFieldName(head)}`;
1804
- return { expr: scalar === "String" ? `${field}.clone()` : field, scalar };
1805
- }
1806
- let acc;
1807
- let expr;
1808
- try {
1809
- const r = typedFieldAccess(baseExpr, baseRef, restPath, plan);
1810
- expr = r.expr;
1811
- acc = r.ref;
1812
- }
1813
- catch {
1814
- return null;
1815
- }
1816
- if (acc.kind !== "scalar" || acc.scalar === "null")
1817
- return null;
1818
- // OWNED value. When restPath is EMPTY, `expr` is a bare REFERENCE (`oel_${s}` = &Elem-scalar, or
1819
- // `cell.borrow()` = Ref<scalar>) — deref with `*` (Copy) / `.clone()` via deref (String). When a
1820
- // `.field` was appended, `expr` is a value PLACE (auto-deref) — read it directly (`.clone()` String).
1821
- const isBareRef = restPath.length === 0;
1822
- let owned;
1823
- if (acc.scalar === "string") {
1824
- owned = isBareRef ? `(*${expr}).clone()` : `${expr}.clone()`;
1825
- }
1826
- else {
1827
- owned = isBareRef ? `*${expr}` : expr;
1828
- }
1829
- return { expr: owned, scalar: rustScalar(acc.scalar) };
1830
- };
1452
+ const asBinding = { name: asName, ref: overElemRef, plan };
1831
1453
  // build the per-element CONCRETE native ports struct. `il` = indent inside the element loop.
1832
1454
  const buildPorts = (il) => {
1833
- const out = [];
1834
- const inits = [];
1835
- const asBinding = { name: asName, ref: overElemRef, plan };
1836
- for (const pn of portNames) {
1837
- const ty = portFieldRustType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding, plan);
1838
- // #108-map: pass the element binding + base expr so a `$as` MAP/NAMED field port clones the typed composite.
1839
- inits.push(emitPortInit(pn, m.ports[pn], ty, resolveRef, comp.inputPorts, plan, undefined, asBinding, `oel_${s}`));
1840
- }
1841
- out.push(`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`);
1842
- return out;
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(", ")} };`];
1843
1457
  };
1844
1458
  // #108 guard: compile `when` to a native bool over an element-scoped resolveHead ($as element value,
1845
1459
  // prior node, input port). On skip: `continue` (element excluded — matches run_behavior's keep-gate).
1846
1460
  const emitGuard = (il) => {
1847
1461
  if (!hasGuard)
1848
1462
  return [];
1849
- const resolveHead = (head) => {
1850
- if (head === asName)
1851
- return { expr: `oel_${s}.clone()`, ref: overElemRef };
1852
- if (priorHere(head))
1853
- return { expr: `${typedCell(head)}.borrow().clone()`, ref: typedNodes.get(head) };
1854
- return rustInputHeadRef(head, comp, plan);
1855
- };
1463
+ const resolveHead = nativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` });
1856
1464
  const g = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compileBool(m.when);
1857
1465
  const out = [];
1858
1466
  for (const st of g.stmts)
@@ -1975,55 +1583,9 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
1975
1583
  if (!dkField || dkField.type.kind !== "scalar" || dkField.type.scalar !== "string")
1976
1584
  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}'.`);
1977
1585
  const dkRust = rustFieldName(f.dedupeKey);
1978
- const resolveRef = (head, restPath, opt) => {
1979
- if (opt)
1980
- return null;
1981
- let baseExpr;
1982
- let baseRef;
1983
- if (head === asName) {
1984
- baseExpr = `oel_${s}`;
1985
- baseRef = overElemRef;
1986
- }
1987
- else if (priorHere(head)) {
1988
- baseExpr = `${typedCell(head)}.borrow()`;
1989
- baseRef = typedNodes.get(head);
1990
- }
1991
- else {
1992
- if (restPath.length !== 0)
1993
- return null;
1994
- const scalar = inputScalarKind(comp.inputPorts?.[head]);
1995
- if (scalar === undefined)
1996
- return null;
1997
- const field = `in_.${rustFieldName(head)}`;
1998
- return { expr: scalar === "String" ? `${field}.clone()` : field, scalar };
1999
- }
2000
- let acc;
2001
- let expr;
2002
- try {
2003
- const r = typedFieldAccess(baseExpr, baseRef, restPath, plan);
2004
- expr = r.expr;
2005
- acc = r.ref;
2006
- }
2007
- catch {
2008
- return null;
2009
- }
2010
- if (acc.kind !== "scalar" || acc.scalar === "null")
2011
- return null;
2012
- const isBareRef = restPath.length === 0;
2013
- let owned;
2014
- if (acc.scalar === "string")
2015
- owned = isBareRef ? `(*${expr}).clone()` : `${expr}.clone()`;
2016
- else
2017
- owned = isBareRef ? `*${expr}` : expr;
2018
- return { expr: owned, scalar: rustScalar(acc.scalar) };
2019
- };
1586
+ const asBinding = { name: asName, ref: overElemRef, plan };
2020
1587
  const buildPorts = (il) => {
2021
- const inits = [];
2022
- const asBinding = { name: asName, ref: overElemRef, plan };
2023
- for (const pn of portNames) {
2024
- const ty = portFieldRustType(f.ports[pn], comp.inputPorts, `fanout port '${pn}' of node '${node.id}'`, asBinding, plan);
2025
- inits.push(emitPortInit(pn, f.ports[pn], ty, resolveRef, comp.inputPorts, plan));
2026
- }
1588
+ const inits = portNames.map((pn) => emitPortInit(pn, f.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
2027
1589
  return [`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`];
2028
1590
  };
2029
1591
  const lines = [];
@@ -2077,24 +1639,72 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
2077
1639
  lines.push(c);
2078
1640
  return lines.join("\n");
2079
1641
  }
2080
- /** #108: resolve an INPUT port head for a native-expr ref (rust). Scalar → `in_.<field>` (owned: String
2081
- * cloned, Copy as-is); array WITH elemType `in_.<field>.clone()` of Vec<ElemT> (for `len`); else null. */
2082
- function rustInputHeadRef(head, comp, plan) {
2083
- const schema = comp.inputPorts?.[head];
2084
- if (schema === undefined)
2085
- return null;
2086
- const sc = inputScalarKind(schema);
2087
- if (sc !== undefined) {
2088
- const scalarRef = { kind: "scalar", scalar: sc === "i64" ? "int" : sc === "f64" ? "float" : sc === "String" ? "string" : "bool" };
2089
- const field = `in_.${rustFieldName(head)}`;
2090
- return { expr: sc === "String" ? `${field}.clone()` : field, ref: scalarRef };
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);
2091
1702
  }
2092
- const et = schema.elemType;
2093
- if ((schema.type === "array" || schema.type === "arr") && et !== undefined) {
2094
- // no clone: the only array use inside a cond/guard expr is `len`, which BORROWS (`&in_.xs`).
2095
- return { expr: `in_.${rustFieldName(head)}`, ref: { kind: "arr", elem: deriveTypeRef(et, plan) } };
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}`);
2096
1707
  }
2097
- return null;
2098
1708
  }
2099
1709
  /**
2100
1710
  * emitCondArm — the struct-native exec of a covered cond node (#108, rust). Mirrors run_behavior's cond
@@ -2106,11 +1716,7 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
2106
1716
  const c = node.cond;
2107
1717
  const s = sanitize(node.id);
2108
1718
  const priorHere = (head) => priorNodeAt(head, atPos);
2109
- const resolveHead = (head) => {
2110
- if (priorHere(head))
2111
- return { expr: `${typedCell(head)}.borrow().clone()`, ref: typedNodes.get(head) };
2112
- return rustInputHeadRef(head, comp, plan);
2113
- };
1719
+ const resolveHead = nativeResolveHead(comp, plan, typedNodes, priorHere);
2114
1720
  const compiler = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan));
2115
1721
  const cond = compiler.compileBool(c.if);
2116
1722
  // #125: a when(...) control-gate cond は出力データ型を持たない制御スキップマーカー。typed cell へ何も
@@ -2249,17 +1855,20 @@ function emit(ctx) {
2249
1855
  // rust CONSUMES this annotation (async fn + .await where derived); go ignores it. per-node granularity
2250
1856
  // is a strict generalization of increment-1's uniform ioModel:'async' (= every terminal async).
2251
1857
  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);
2252
1861
  for (const c of ctx.ir.components)
2253
- assertTypedCoverage(c);
2254
- const native = ctx.ir.components.filter(isNativeRaw);
2255
- const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
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));
2256
1865
  // FAIL CLOSED (bc#86/#89): the --typed-native (native codegen) surface is native-only. Previously a
2257
1866
  // non-covered component was SILENTLY DROPPED (emitted only as a comment note), concealing that it
2258
1867
  // never got native codegen. We now throw a LOUD GeneratorFailure naming every uncovered component +
2259
1868
  // its specific reject reason. A fully-covered module is unchanged.
2260
1869
  if (nonNative.length > 0) {
2261
1870
  const rejects = nonNative
2262
- .map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
1871
+ .map((c) => ` - component '${c.name}': ${coverageRejectReason(c, plan) ?? "not a covered native read"}`)
2263
1872
  .join("\n");
2264
1873
  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` +
2265
1874
  `The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
@@ -2273,7 +1882,6 @@ function emit(ctx) {
2273
1882
  ${emitStructSurface([])}
2274
1883
  `;
2275
1884
  }
2276
- const plan = buildTypePlan(ctx.ir);
2277
1885
  // The typed-native module is a de-boxed CONSUMER artifact: the covered runner RETURNS these outType
2278
1886
  // structs and the consumer reads their fields natively. So the decls (and their FIELDS) are pub — the
2279
1887
  // consumer keeps the model native (mirrors Go's exported outType struct fields on the covered path).
@@ -2291,11 +1899,7 @@ ${emitStructSurface([])}
2291
1899
  const inStructs = [];
2292
1900
  const elemMaterializers = [];
2293
1901
  for (const c of native) {
2294
- const typedNodes = new Map();
2295
- // build the full typed-node map FIRST so a map's over-element resolution can see every node.
2296
- for (const n of c.body)
2297
- if (!isControlGate(n))
2298
- typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan)); // #125: control-gate typeless; map は produced-array ref に正規化
1902
+ const typedNodes = buildTypedNodes(c, plan);
2299
1903
  for (const n of c.body) {
2300
1904
  if ("fanout" in n)
2301
1905
  structs.push(emitFanoutPortsStructs(c, n, typedNodes, plan));
@@ -2305,7 +1909,7 @@ ${emitStructSurface([])}
2305
1909
  // #108: a cond node has NO ports struct (pure Expression — no handler dispatch).
2306
1910
  }
2307
1911
  else
2308
- structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
1912
+ structs.push(emitPortsStruct(c, n, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
2309
1913
  }
2310
1914
  rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
2311
1915
  handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
@@ -2370,8 +1974,8 @@ use std::cell::RefCell;`;
2370
1974
  */
2371
1975
  export function rustTypedNativeObserve(ir, ioModel = "sync") {
2372
1976
  const components = ir.components;
2373
- const native = components.filter(isNativeRaw);
2374
1977
  const plan = buildTypePlan(ir);
1978
+ const native = components.filter((c) => isNativeRaw(c, plan));
2375
1979
  const needSync = native.some((c) => concurrencyPlan(c) !== null);
2376
1980
  const serializers = emitSerializers(plan);
2377
1981
  const marshallers = emitMarshallers(plan);
@@ -2469,17 +2073,27 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
2469
2073
  const node = n;
2470
2074
  const rowT = rawRowStructName(c.name, node.id);
2471
2075
  const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
2472
- // #129 (TEST glue): if this node has an `items` port and its outType is a bare arr, support the
2473
- // reference scriptedHandlers' `echo === "items"` the scripted handler ECHOES the NATIVE `f_items`
2474
- // port back as its result. This makes the node→leaf array dataflow test non-vacuous: the leaf's
2475
- // observed result is exactly the native array that flowed into its ports struct, so a dropped or
2476
- // boxed native array would diverge from run_behavior. `ports` is bound only on the echo path.
2477
- const canEcho = ref.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items");
2478
- const portsParam = canEcho ? "ports" : "_ports";
2479
- const echoBranch = canEcho
2480
- ? `\n if echo == "items" {\n return Some(${rowT} { val: ${portsParam}.${portFieldName("items")}.clone(), ..Default::default() });\n }`
2481
- : "";
2482
- const okBind = canEcho ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
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)";
2483
2097
  methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
2484
2098
  let ${okBind} = self.next(${rustStrLit(node.component)})?;
2485
2099
  if is_err {
@@ -2548,7 +2162,14 @@ impl ScriptedNativeRaw {
2548
2162
  let mut queues = self.queues.lock().unwrap();
2549
2163
  let (queue, single) = queues.get_mut(component)?;
2550
2164
  let raw = if *single || queue.len() <= 1 { queue[0].clone() } else { queue.remove(0) };
2551
- let echo = raw.get("echo").and_then(|e| e.as_str()).unwrap_or("").to_string();
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(),
2172
+ };
2552
2173
  if let Some(okv) = raw.get("ok") {
2553
2174
  let v = behavior_contracts::decode_value(okv).expect("decode ok");
2554
2175
  Some((false, String::new(), Some(v), echo))
@@ -2639,8 +2260,10 @@ ${arms}
2639
2260
  }
2640
2261
  /** emitInDecoder — a TEST-glue decoder `decode_InNR_<comp>(input) -> Result<InNR_<comp>, BehaviorError>`.
2641
2262
  * Reads each declared input port from the generic &[(String,Value)] into the concrete field (scalar →
2642
- * typed read; Value-typed → the raw Value clone). A missing key leaves Default (every covered input is
2643
- * required + supplied by the corpus). Lives ONLY in the observe companion. */
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. */
2644
2267
  function emitInDecoder(comp, plan) {
2645
2268
  const name = inStructName(comp.name);
2646
2269
  const ports = Object.keys(comp.inputPorts ?? {});
@@ -2662,7 +2285,15 @@ function emitInDecoder(comp, plan) {
2662
2285
  const et = schema.elemType;
2663
2286
  const head = i === 0 ? " if" : " } else if";
2664
2287
  lines.push(`${head} k == ${rustStrLit(p)} {`);
2665
- if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
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) {
2666
2297
  // #108: decode the boxed Value::Arr into the native Vec<ElemT> (test glue — materialize each elem).
2667
2298
  const elemRef = deriveTypeRef(et, plan);
2668
2299
  const elemTy = renderTypeRef(elemRef);
@@ -2700,6 +2331,14 @@ function emitInDecoder(comp, plan) {
2700
2331
  lines.push(`}`);
2701
2332
  return lines.join("\n");
2702
2333
  }
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
+ }
2703
2342
  /** emitDecodeRow — emit (once, deduped) a decoder `decode_row_<comp>_<node><suffix>(v: &Value) -> <rowT>`
2704
2343
  * that materializes the scripted ok Value into the concrete row struct. Named: reuse the Value->struct
2705
2344
  * marshaller + move fields; scalar/arr/opt: materialize into .val. TEST glue only. */