behavior-contracts 0.8.4 → 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.
@@ -88,240 +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
- // a ref to a REQUIRED scalar input port → that scalar. An OPTIONAL port has no required-scalar
139
- // kind (its native type is Option<T>) and is lowered by the opt lane ahead of this — reaching here
140
- // with one means the shape is uncovered, and "Value" routes it to the caller's loud fail-closed.
141
- if (e.path.length === 1) {
142
- const sc = inputScalarKind(inputPorts[e.path[0]]);
143
- if (sc !== undefined)
144
- return sc;
145
- }
146
- return "Value";
147
- }
148
- default:
149
- return "Value";
150
- }
151
- }
152
- /** A static string-array port (every element a static string literal) — the graphddb `projection`
153
- * shape. Returns the list of literal strings, or null when the node is not that shape. */
154
- function staticStringArrElems(node) {
155
- const arr = staticArrElems(node);
156
- if (arr === null)
157
- return null;
158
- const out = [];
159
- for (const el of arr) {
160
- const e = classifyExpr(el);
161
- if (e.kind !== "str")
162
- return null;
163
- out.push(e.value);
164
- }
165
- return out;
166
- }
167
- /**
168
- * portFieldRustType — the NATIVE Rust type for a covered port field (bc#90 / runtime-free). The covered
169
- * ports struct carries NO `Value` field: every port lowers to a concrete Rust type.
170
- * - a static string-array (projection) → `Vec<&'static str>` (change #2);
171
- * - a bare int/float number literal (e.g. Query `limit: 20`) → `i64` / `f64`;
172
- * - a string/bool/scalar-input port → the scalar Rust type (inferPortType).
173
- * A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
174
- * Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no `Value`
175
- * 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.
176
96
  */
177
- function portFieldRustType(node, inputPorts, where = "port", asBinding, plan, typedNodes) {
178
- // A port reading an OPTIONAL input port lowers through the shared native-expr compiler (the
179
- // Option<T> pass-through / the coalesce default). It runs FIRST so an optional input can never fall
180
- // into the required-scalar inference below (which has no way to say "absent").
181
- const optPort = optPortCompileR(node, inputPorts, where, plan);
182
- if (optPort !== null)
183
- return renderTypeRef(optPort.ref);
184
- if (staticStringArrElems(node) !== null)
185
- return "Vec<&'static str>";
186
- // #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → Vec<ElemT>.
187
- const arrElem = portArrayElemRef(node, inputPorts, plan);
188
- if (arrElem !== null)
189
- return `Vec<${renderTypeRef(arrElem)}>`;
190
- // #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) → Vec<ElemT>.
191
- const priorArr = priorNodeArrElemInfo(node, typedNodes, plan);
192
- if (priorArr !== null)
193
- return `Vec<${renderTypeRef(priorArr.elemRef)}>`;
194
- const num = numLiteralRustExpr(node);
195
- if (num !== null)
196
- return typeof node === "number" && Number.isInteger(node) ? "i64" : "f64";
197
- // a ref to a `$as` element field (or an input port) whose type is a MAP or NAMED struct (NOT a bare
198
- // scalar) → the native map/struct type. #108-map: a `{map:V}`-typed field (e.g. a nested
199
- // `map<map<obj>>` write port) lowers to BTreeMap; a named field lowers to its struct. Reuses the same
200
- // typedFieldAccess resolution as the scalar path; a genuinely-untyped ref still falls through.
201
- const composite = portCompositeRef(node, inputPorts, asBinding, plan);
202
- if (composite !== null)
203
- return renderTypeRef(composite);
204
- const ft = inferPortType(node, inputPorts, asBinding);
205
- if (ft === "Value") {
206
- 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. ${RUST_STATIC_PORT_FORMS} Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
207
- }
208
- return ft;
209
- }
210
- /**
211
- * optPortCompileR — the OPTIONAL-input port lane (rust). A component may declare an input port
212
- * `{opt:T}` (PortSchema `required:false`); the only native representation of "the value is absent" is
213
- * `Option<T>`, so such a port never lowers through the required-scalar inference. This lane compiles the
214
- * port expression with the SHARED runtime-free native-expr compiler (native-expr.ts — the same traversal,
215
- * type-inference and fail-closed discipline the go twin uses, so the two agree by construction):
216
- *
217
- * - `opt($.x)` / `$.x` at port position → `Option<T>` (the leaf receives the absent value as null,
218
- * exactly as run_behavior's `evalPorts` passes it).
219
- * - `coalesce(opt($.x), <literal>)` → `in_.x.unwrap_or(<literal>)` — a native `T`, no boxed Value.
220
- * - `ne(opt($.x), null)` / `eq(…, null)` → the native presence test (`is_some()` / `is_none()`).
221
- *
222
- * Returns null when the port expression reads NO optional input (the caller keeps its existing lowering).
223
- * A port that DOES read one but does not compile — or that compiles to a fallible expression, which this
224
- * position cannot hoist — FAILS CLOSED loudly rather than degrade to a required type or a zero value.
225
- */
226
- function optPortCompileR(node, inputPorts, where, plan) {
227
- if (plan === undefined)
228
- return null;
229
- if (!exprReadsOptionalInput(node, inputPorts))
230
- return null;
231
- const resolveHead = (head) => rustInputHeadRef(head, { inputPorts }, plan);
232
- let c;
233
- try {
234
- c = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compilePort(node);
235
- }
236
- catch (e) {
237
- if (!(e instanceof GeneratorFailure))
238
- throw e;
239
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} ${JSON.stringify(node)} reads an OPTIONAL input port but does not lower to a native Rust value: ${e.message}`);
240
- }
241
- if (c.stmts.length > 0)
242
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} ${JSON.stringify(node)} reads an OPTIONAL input port through 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.`);
243
- return c;
244
- }
245
- /** exprReadsOptionalInput — does this port expression reference an OPTIONAL (`required:false`) input
246
- * port? Language-neutral scan for `ref`/`refOpt` heads bound to an optional port. Decides ONLY which
247
- * lane a port takes; the lane itself then proves the lowering (or fails closed). */
248
- function exprReadsOptionalInput(node, inputPorts) {
249
- if (node === null || typeof node !== "object")
250
- return false;
251
- if (Array.isArray(node))
252
- return node.some((el) => exprReadsOptionalInput(el, inputPorts));
253
- const rec = node;
254
- const keys = Object.keys(rec);
255
- if (keys.length === 1 && (keys[0] === "ref" || keys[0] === "refOpt")) {
256
- const path = rec[keys[0]];
257
- return Array.isArray(path) && typeof path[0] === "string" && inputPortIsOptional(inputPorts?.[path[0]]);
258
- }
259
- return keys.some((k) => exprReadsOptionalInput(rec[k], inputPorts));
260
- }
261
- /** portCompositeRef — the full TypeRef of a port that is a `{ref:[...]}` into a `$as` element field OR
262
- * an input port whose type is a MAP or NAMED struct (a non-scalar covered field). Returns null when the
263
- * node is not such a ref (so the caller falls through to the scalar/array inference). #108-map: enables a
264
- * `{map:V}`-typed field port (e.g. a nested map write port) to lower to the native BTreeMap. */
265
- function portCompositeRef(node, inputPorts, asBinding, plan) {
266
- const e = classifyExpr(node);
267
- if (e.kind !== "ref" || plan === undefined)
268
- return null;
269
- let ref = null;
270
- if (asBinding !== undefined && e.path[0] === asBinding.name) {
271
- try {
272
- ref = typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan).ref;
273
- }
274
- catch {
275
- return null;
276
- }
277
- }
278
- else if (e.path.length >= 1) {
279
- const s = inputPorts[e.path[0]];
280
- // a whole-port ref to an input MAP port carrying an elemType → its BTreeMap type (mirrors
281
- // inputPortRustType); a deeper path into an input port is not resolved here (no field type source).
282
- if (s !== undefined && s.type === "map" && e.path.length === 1) {
283
- const et = s.elemType;
284
- if (et !== undefined)
285
- return { kind: "map", value: deriveTypeRef(et, plan) };
286
- }
287
- return null;
288
- }
289
- if (ref === null)
290
- return null;
291
- // only MAP / NAMED composite fields here — scalars/arrays keep their existing dedicated paths.
292
- if (ref.kind === "map" || ref.kind === "named")
293
- return ref;
294
- return null;
295
- }
296
- /** emitCompositePortValue — the OWNED native value expr for a port that is a ref to a `$as` element
297
- * MAP/NAMED field (or an input map port): a typed field access `.clone()` (the concrete BTreeMap/struct,
298
- * ZERO boxed Value). `elemBaseExpr` is the map arm's element base (`oel_<mapId>`) for a `$as`-headed ref;
299
- * omitted at the top level (only input-port composite refs resolve there). Returns null otherwise. */
300
- function emitCompositePortValue(node, inputPorts, asBinding, plan, elemBaseExpr) {
301
- const e = classifyExpr(node);
302
- if (e.kind !== "ref")
303
- return null;
304
- if (asBinding !== undefined && elemBaseExpr !== undefined && e.path[0] === asBinding.name) {
305
- let acc;
306
- try {
307
- acc = typedFieldAccess(elemBaseExpr, asBinding.ref, e.path.slice(1), plan);
308
- }
309
- catch {
310
- return null;
311
- }
312
- if (acc.ref.kind !== "map" && acc.ref.kind !== "named")
313
- return null;
314
- return `${acc.expr}.clone()`;
315
- }
316
- // a WHOLE-port ref to an input MAP port (with elemType) — valid at top level OR inside a map node (a
317
- // ref head that is NOT the `$as` element binding). Clones the native BTreeMap off the input struct.
318
- if ((asBinding === undefined || e.path[0] !== asBinding.name) && e.path.length === 1) {
319
- const s = inputPorts[e.path[0]];
320
- if (s !== undefined && s.type === "map" && s.elemType !== undefined) {
321
- return `in_.${rustFieldName(e.path[0])}.clone()`;
322
- }
323
- }
324
- 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);
325
100
  }
326
101
  /**
327
102
  * emitPortsStruct — the CONCRETE native ports struct for a componentRef node. Typed fields per the
@@ -330,13 +105,13 @@ function emitCompositePortValue(node, inputPorts, asBinding, plan, elemBaseExpr)
330
105
  * reads `ports.f_<field>` DIRECTLY. The struct derives Clone (a bc#87 parallel stage clones it into the
331
106
  * per-worker dispatch). Every field is a NATIVE Rust type (bc#90 / runtime-free) — NO boxed `Value`.
332
107
  */
333
- function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
108
+ function emitPortsStruct(comp, node, plan, typedNodes, asBinding) {
334
109
  const name = portsStructName(comp.name, node.id);
335
110
  const portNames = Object.keys(node.ports);
336
111
  if (portNames.length === 0) {
337
112
  return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
338
113
  }
339
- 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));
340
115
  const fields = portNames
341
116
  .map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
342
117
  .join("\n");
@@ -360,7 +135,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
360
135
  // #108: element ports may read the `$as` binding — resolve the over element type for native typing.
361
136
  const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
362
137
  const asBinding = { name: m.as, ref: elemRef, plan };
363
- 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);
364
139
  return `${elemStruct}
365
140
 
366
141
  // ${batch} — CONCRETE batched ports for map '${node.id}': the Vec of per-element CONCRETE ports structs
@@ -378,7 +153,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
378
153
  const batch = `${name}Batch`;
379
154
  const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
380
155
  const asBinding = { name: f.as, ref: elemRef, plan };
381
- 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);
382
157
  return `${elemStruct}
383
158
 
384
159
  // ${batch} — CONCRETE batched ports for fanout '${node.id}': the Vec of per-id CONCRETE ports structs.
@@ -441,6 +216,9 @@ function makeRustExprBackend(resolveHead, plan) {
441
216
  notOp: (a) => `(!${a})`,
442
217
  structLit: (name, inits) => `${name} { ${inits.map((i) => `${rustFieldName(i.field)}: ${i.expr}`).join(", ")} }`,
443
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>" }),
444
222
  // an opt (`Option<T>`) NONE literal. Annotate as `Option::<T>::None` so it type-infers in ANY
445
223
  // position (a bare `None` can be ambiguous; the explicit turbofish keeps it a fully-typed value).
446
224
  optNone: (innerTy) => `Option::<${innerTy}>::None`,
@@ -869,44 +647,6 @@ function mapOverElemInfo(comp, node, typedNodes, plan) {
869
647
  function mapOverElemType(comp, node, typedNodes, plan) {
870
648
  return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
871
649
  }
872
- /**
873
- * priorNodeArrElemInfo (#129, rust twin) — resolve a leaf port that is a `ref` into a PRIOR BODY NODE
874
- * whose annotated `outType` is (or reaches, through a field path) a native array, to its element TypeRef
875
- * + the OWNED Rust Vec expression that yields it. GENERALIZES the `map…over` prior-node-arr dataflow
876
- * (mapOverElemInfo's first branch) to ANY leaf input port: a fold/dedup leaf whose `items` port is
877
- * `{ref:["<priorNode>"]}` (or `.items` of a prior connection node) now lowers to `Vec<ElemT>` cloned
878
- * out of the prior node's typed cell — ZERO boxed Value on the read hot path.
879
- *
880
- * The element type comes STRICTLY from the prior node's EXPLICIT `outType` annotation walked through
881
- * typedFieldAccess (BC does NOT infer — consumer-interface C3 / [[graphddb-typed-native-static-link]]):
882
- * if the head is not a prior body node, or its outType (through the field path) is not an `arr`, this
883
- * returns null and the caller keeps its existing fail-closed behaviour (never a silent box). A
884
- * single-segment INPUT-array port (elemType) is a DIFFERENT covered shape (portArrayElemRef) — handled
885
- * ahead of this. Requires the typedNodes map (prior-node outType TypeRefs) and a TypePlan.
886
- */
887
- function priorNodeArrElemInfo(node, typedNodes, plan) {
888
- if (typedNodes === undefined || plan === undefined)
889
- return null;
890
- if (staticArrElems(node) !== null)
891
- return null; // a static array literal — not a prior-node ref.
892
- const e = classifyExpr(node);
893
- if (e.kind !== "ref" || e.opt || e.path.length < 1)
894
- return null;
895
- if (!typedNodes.has(e.path[0]))
896
- return null; // head is not a prior body node → not this shape.
897
- const baseRef = typedNodes.get(e.path[0]);
898
- let acc;
899
- try {
900
- acc = typedFieldAccess(`${typedCell(e.path[0])}.borrow()`, baseRef, e.path.slice(1), plan);
901
- }
902
- catch {
903
- return null; // the field path does not resolve on the prior node's outType → fail-closed at caller.
904
- }
905
- if (acc.ref.kind !== "arr")
906
- return null; // the prior-node result (through the path) is not an array.
907
- // OWNED: clone the borrowed slice out of the RefCell so the ports struct owns its Vec<ElemT>.
908
- return { elemRef: acc.ref.elem, overExpr: `${acc.expr}.clone()` };
909
- }
910
650
  // ── eligibility ─────────────────────────────────────────────────────────────────────
911
651
  function reconstructR(e) {
912
652
  switch (e.kind) {
@@ -924,56 +664,35 @@ function reconstructR(e) {
924
664
  return e.node;
925
665
  }
926
666
  }
927
- /**
928
- * numLiteralRustExpr a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
929
- * `classifyExpr` leaves a bare number `dynamic` (its checked int/float range rules are the evaluate
930
- * SSoT), so it never reaches the static str/bool/null branch — but a number literal IS statically
931
- * resolvable to a native `Value`: the SAME range check the runtime applies (integral literal in the
932
- * safe 2^53-1 window Value::Int; a fractional/exponent literal the checked Value::Float) is
933
- * performed HERE at generation time. Returns the Rust expression for the literal's Value, or null
934
- * when the node is not a bare number OR an integral literal is out of the safe window (keeping the
935
- * runtime INVALID_LITERAL semantics — the port is not covered natively, the component falls through).
936
- * Mirrors behavior_contracts eval of a bare number literal exactly.
937
- */
938
- const NUM_SAFE_INT_R = 9007199254740991; // 2^53 - 1 (expr safeInt)
939
- function numLiteralRustExpr(node) {
940
- if (typeof node !== "number" || !Number.isFinite(node))
941
- return null;
942
- if (Number.isInteger(node)) {
943
- if (node < -NUM_SAFE_INT_R || node > NUM_SAFE_INT_R)
944
- return null; // out of safe window → not covered
945
- return `Value::Int(${node}i64)`;
946
- }
947
- // fractional / exponent literal → checked f64 (finite, guaranteed above).
948
- return `Value::Float(${node}f64)`;
949
- }
950
- /** A port node is static (literal / ref / concat of those / static arr / bare number literal / an
951
- * optional-input read) — resolvable to a native value with no Obj / no evaluate interpreter. This is the
952
- * SHAPE gate that decides native ELIGIBILITY; the lowering itself (portFieldRustType / emitPortInit) is
953
- * what proves each shape, and fails closed loudly on one it cannot lower. */
954
- function portIsStatic(node, inputPorts) {
955
- const arr = staticArrElems(node);
956
- if (arr !== null)
957
- return arr.every((el) => portIsStatic(el, inputPorts));
958
- if (numLiteralRustExpr(node) !== null)
959
- return true; // bare number literal (e.g. Query `limit`)
960
- // An optional-input read (`opt($.x)` / `coalesce(opt($.x), 20)` / `ne(opt($.x), null)`) is a
961
- // statically-resolvable port — the opt lane lowers it (or fails closed with the specific reason).
962
- if (inputPorts !== undefined && exprReadsOptionalInput(node, inputPorts))
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);
963
673
  return true;
964
- const e = classifyExpr(node);
965
- switch (e.kind) {
966
- case "str":
967
- case "bool":
968
- case "null":
969
- case "ref":
970
- return true;
971
- case "concat":
972
- return e.parts.every((p) => portIsStatic(reconstructR(p), inputPorts));
973
- default:
974
- return false;
674
+ }
675
+ catch {
676
+ return false; // an eligibility probe: any lowering failure ⇒ not native-covered here.
975
677
  }
976
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
+ }
977
696
  /** The output must lower to a typed struct/value with no scope read: a ref to a BODY NODE, an
978
697
  * obj/arr of such, or a concat. A ref to an input param / non-concat operator is not lowerable. */
979
698
  function outputIsNativeLowerable(comp) {
@@ -1005,7 +724,7 @@ function outputIsNativeLowerable(comp) {
1005
724
  /**
1006
725
  * coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet), rust twin of the go predicate.
1007
726
  */
1008
- function coveredMapNode(n, bodyIds, comp) {
727
+ function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
1009
728
  if (n === null || typeof n !== "object" || !("map" in n))
1010
729
  return false;
1011
730
  const m = n.map;
@@ -1037,14 +756,25 @@ function coveredMapNode(n, bodyIds, comp) {
1037
756
  return false;
1038
757
  }
1039
758
  const ports = m.ports;
759
+ const asBinding = mapAsBinding(comp, n, typedNodes, plan);
1040
760
  for (const p of Object.keys(ports))
1041
- if (!portIsStatic(ports[p], comp.inputPorts))
761
+ if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
1042
762
  return false;
1043
763
  return true;
1044
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
+ }
1045
775
  /** coveredFanoutNode (v3, rust twin) — a first-class fanout node is covered when its over is a covered
1046
776
  * ref (prior-node arr / input array with elemType) and its element ports are static. See the go twin. */
1047
- function coveredFanoutNode(n, bodyIds, comp) {
777
+ function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
1048
778
  if (n === null || typeof n !== "object" || !("fanout" in n))
1049
779
  return false;
1050
780
  const f = n.fanout;
@@ -1064,8 +794,15 @@ function coveredFanoutNode(n, bodyIds, comp) {
1064
794
  return false;
1065
795
  }
1066
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
+ }
1067
804
  for (const p of Object.keys(ports))
1068
- if (!portIsStatic(ports[p], comp.inputPorts))
805
+ if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
1069
806
  return false;
1070
807
  return true;
1071
808
  }
@@ -1101,8 +838,8 @@ function fanoutOverElemInfo(comp, node, typedNodes, plan) {
1101
838
  }
1102
839
  /** A covered read SHAPE with static ports + native-lowerable output (+ covered map...into), OR a
1103
840
  * real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point reads. */
1104
- function isSequentialComponentRefRead(comp) {
1105
- return coverageRejectReason(comp) === null;
841
+ function isSequentialComponentRefRead(comp, plan) {
842
+ return coverageRejectReason(comp, plan) === null;
1106
843
  }
1107
844
  /**
1108
845
  * coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89), rust twin of
@@ -1111,10 +848,11 @@ function isSequentialComponentRefRead(comp) {
1111
848
  * IDENTICAL to isSequentialComponentRefRead (defined as `coverageRejectReason(c) === null`) so the two
1112
849
  * can never disagree. This is the message the fail-closed dispatch surfaces per non-native component.
1113
850
  */
1114
- function coverageRejectReason(comp) {
851
+ function coverageRejectReason(comp, plan) {
1115
852
  const ep = execPlan(comp);
1116
853
  if (ep === null)
1117
854
  return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
855
+ const typedNodes = buildTypedNodes(comp, plan);
1118
856
  // #114: a bindField relation child (single|connection) IS covered inside a parallel stage — the
1119
857
  // sibling hasMany fan-out (parent → members ∥ permissions). Its bound-key skip-gate is preflighted
1120
858
  // per member (from the settled parent) BEFORE dispatch, so the scoped-thread fan-out keeps
@@ -1152,12 +890,12 @@ function coverageRejectReason(comp) {
1152
890
  continue;
1153
891
  }
1154
892
  if ("fanout" in n) {
1155
- if (!coveredFanoutNode(n, bodyIds, comp))
893
+ if (!coveredFanoutNode(n, bodyIds, comp, plan, typedNodes))
1156
894
  return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
1157
895
  continue;
1158
896
  }
1159
897
  if ("map" in n) {
1160
- if (!coveredMapNode(n, bodyIds, comp))
898
+ if (!coveredMapNode(n, bodyIds, comp, plan, typedNodes))
1161
899
  return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
1162
900
  continue;
1163
901
  }
@@ -1176,7 +914,7 @@ function coverageRejectReason(comp) {
1176
914
  if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
1177
915
  return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
1178
916
  for (const p of Object.keys(cr.ports))
1179
- if (portIsStatic(cr.ports[p], comp.inputPorts) === false)
917
+ if (portIsStatic(cr.ports[p], comp, plan, typedNodes) === false)
1180
918
  return `node '${n.id}' port '${p}' is not statically resolvable`;
1181
919
  }
1182
920
  if (!outputIsNativeLowerable(comp))
@@ -1193,11 +931,11 @@ function isFullyTyped(comp) {
1193
931
  return false;
1194
932
  return true;
1195
933
  }
1196
- function isNativeRaw(comp) {
1197
- return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
934
+ function isNativeRaw(comp, plan) {
935
+ return isSequentialComponentRefRead(comp, plan) && isFullyTyped(comp);
1198
936
  }
1199
- function assertTypedCoverage(comp) {
1200
- if (!isSequentialComponentRefRead(comp))
937
+ function assertTypedCoverage(comp, plan) {
938
+ if (!isSequentialComponentRefRead(comp, plan))
1201
939
  return;
1202
940
  if (isFullyTyped(comp))
1203
941
  return;
@@ -1264,172 +1002,19 @@ ${fields}
1264
1002
  }
1265
1003
  /** The port forms the STATIC (non-optional) lane lowers — named in its reject message so the message
1266
1004
  * describes what is actually covered rather than a stale corpus list. */
1267
- const RUST_STATIC_PORT_FORMS = "This port does not read an optional input, so it lowers through the STATIC port forms: a string / bool / number literal, a static string-array (projection), a ref to a REQUIRED scalar input port, a ref to an input array/map port or a prior node's array (declared elemType), a ref to a `$as` element field, or a concat of native strings. A dynamic operator at port position is not covered on this lane. (A port that DOES read an optional input lowers through the full native expression compiler, which admits the whole closed operator set — so the covered operator set at port position currently depends on the lane, not on the operator.)";
1268
1005
  /**
1269
- * emitNativeScalarlower a static port expr to a NATIVE Rust expression of the concrete `want` scalar
1270
- * type, or null if it cannot be statically proven that type. A string literal is native for
1271
- * `want==="string"`, a bool literal for `want==="bool"`. A ref resolves via `resolveRef`. A concat lowers
1272
- * ONLY into `string` and only when EVERY part is a native string, emitting a single `format!("{}{}…", …)`.
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.
1273
1010
  */
1274
- function emitNativeScalar(node, want, resolveRef) {
1275
- if (staticArrElems(node) !== null)
1276
- return null;
1277
- const e = classifyExpr(node);
1278
- switch (e.kind) {
1279
- case "str":
1280
- return want === "String" ? `${rustStrLit(e.value)}.to_string()` : null;
1281
- case "bool":
1282
- return want === "bool" ? (e.value ? "true" : "false") : null;
1283
- case "null":
1284
- return null;
1285
- case "ref": {
1286
- const sc = resolveRef(e.path[0], e.path.slice(1), e.opt);
1287
- if (sc === null || sc.scalar !== want)
1288
- return null;
1289
- return sc.expr;
1290
- }
1291
- case "concat": {
1292
- if (want !== "String")
1293
- return null;
1294
- // a single-part concat is just the owned String of that part.
1295
- if (e.parts.length === 1)
1296
- return emitNativeScalar(reconstructR(e.parts[0]), "String", resolveRef);
1297
- // multi-part: fold into ONE format! (statically strings only — the concat's strings-only
1298
- // TYPE_MISMATCH can never fire, so dropping the check is sound). To stay clippy-clean
1299
- // (to_string_in_format_args), a string-literal part is baked into the format STRING itself
1300
- // (escaped) rather than passed as a `{}` arg; a ref part becomes a `{}` arg (Display on String).
1301
- let fmt = "";
1302
- const args = [];
1303
- for (const p of e.parts) {
1304
- const sub = classifyExpr(reconstructR(p));
1305
- if (sub.kind === "str") {
1306
- fmt += sub.value.replace(/[{}]/g, (c) => c + c); // escape { } for the format string
1307
- continue;
1308
- }
1309
- const arg = emitNativeScalar(reconstructR(p), "String", resolveRef);
1310
- if (arg === null)
1311
- return null;
1312
- fmt += "{}";
1313
- args.push(arg);
1314
- }
1315
- if (args.length === 0)
1316
- return `${rustStrLit(fmt)}.to_string()`;
1317
- return `format!(${rustStrLit(fmt)}, ${args.join(", ")})`;
1318
- }
1319
- default:
1320
- return null;
1321
- }
1322
- }
1323
- // bc#90 / runtime-free: the old `emitStaticR` / `inputLeafR` / `valueLeafR` port lowering (which built
1324
- // `Value` leaves via the primitives::* helpers and struct-field serializers) is REMOVED — every covered
1325
- // port now lowers to a FULLY NATIVE Rust value in emitPortInit (string / bool / static-string-array /
1326
- // number literal), and a genuinely-dynamic port fails closed. There is no boxed-Value fallback on the
1327
- // covered read plane.
1328
- /**
1329
- * emitPortInit — emit the port field build for one port as a FULLY NATIVE Rust expression (bc#90 /
1330
- * runtime-free). Every covered port lowers to a concrete Rust value assigned straight into the ports
1331
- * struct field — NO `Value`, NO fallback path, NO `match`:
1332
- * - a static string array (projection) → `vec!["a", "b", ...]` (change #2);
1333
- * - a bare int/float number literal (e.g. Query `limit: 20`) → `20i64` / `<n>f64`;
1334
- * - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
1335
- * A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered plane —
1336
- * a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
1337
- * ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
1338
- */
1339
- function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan, typedNodes, asBinding, elemBaseExpr) {
1340
- const field = portFieldName(pn);
1341
- // The OPTIONAL-input lane (Option<T> pass-through / coalesce default) — same shared compile as
1342
- // portFieldRustType, so the field type and its initializer are derived from ONE lowering.
1343
- const optPort = optPortCompileR(expr, inputPorts ?? {}, `port '${pn}'`, plan);
1344
- if (optPort !== null)
1345
- return `${field}: ${optPort.expr}`;
1346
- // #108-map: a port that is a ref to a `$as` element MAP/NAMED field (or an input map port) → CLONE the
1347
- // typed composite value directly off the element/input (native BTreeMap / struct — ZERO boxed Value).
1348
- // Reuses portCompositeRef for the type; the value is the typed field access `.clone()`.
1349
- if (plan !== undefined) {
1350
- const compExpr = emitCompositePortValue(expr, inputPorts ?? {}, asBinding, plan, elemBaseExpr);
1351
- if (compExpr !== null)
1352
- return `${field}: ${compExpr}`;
1353
- }
1354
- // #110: a componentRef port bound to a runtime array input port (with elemType) → the native Vec<ElemT>,
1355
- // cloned out of the concrete input struct (`in_.<field>.clone()`) — ZERO boxed Value on the read hot path.
1356
- if (inputPorts !== undefined && portArrayElemRef(expr, inputPorts, plan) !== null) {
1357
- const e = classifyExpr(expr);
1358
- if (e.kind === "ref" && e.path.length === 1) {
1359
- return `${field}: in_.${rustFieldName(e.path[0])}.clone()`;
1360
- }
1361
- }
1362
- // #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) →
1363
- // the native Vec<ElemT> cloned off the prior node's typed cell (`t_<node>.borrow().field….clone()`) —
1364
- // ZERO boxed Value on the read hot path (a fold/dedup leaf whose `items` = a prior node's array result).
1365
- const priorArr = priorNodeArrElemInfo(expr, typedNodes, plan);
1366
- if (priorArr !== null)
1367
- return `${field}: ${priorArr.overExpr}`;
1368
- // change #2: a static string array (projection) → a native `vec![...]` of &'static str.
1369
- const strArr = staticStringArrElems(expr);
1370
- if (strArr !== null) {
1371
- if (fieldRustType !== "Vec<&'static str>") {
1372
- 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>).`);
1373
- }
1374
- return `${field}: vec![${strArr.map((s) => rustStrLit(s)).join(", ")}]`;
1375
- }
1376
- // a bare number literal (e.g. Query `limit: 20`): the SSoT range check ran in numLiteralRustExpr —
1377
- // emit the concrete native i64/f64 (unwrap the Value::Int/Float box: it was only there for the old
1378
- // boxed path; the range check is what matters, and it already passed).
1379
- const num = numLiteralRustExpr(expr);
1380
- if (num !== null) {
1381
- if (fieldRustType === "i64")
1382
- return `${field}: ${expr}i64`;
1383
- if (fieldRustType === "f64")
1384
- return `${field}: ${expr}f64`;
1385
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90): port '${pn}' is a number literal but its field type is '${fieldRustType}'.`);
1386
- }
1387
- // string / bool / scalar-input / concat → native scalar expression (no Value, no `match`).
1388
- const native = emitNativeScalar(expr, fieldRustType, resolveRef);
1389
- if (native !== null) {
1390
- return `${field}: ${native}`;
1391
- }
1392
- // FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would re-couple
1393
- // the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the component is not
1394
- // native-eligible and must regenerate on the boxed straight-line path.
1395
- 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. ${RUST_STATIC_PORT_FORMS} 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}`;
1396
1016
  }
1397
1017
  // ── the combined runner (STRUCT-returning; INLINE sequential; native ports in; concrete row out) ──
1398
- /** resolveRefAt for the top-level runner: a ref head is a PRIOR NODE (its typed scalar field) or an
1399
- * INPUT PORT (in_.<field>). A REQUIRED scalar resolves to an OWNED native Rust expression; opt / non-
1400
- * scalar / refOpt → null (fall back to the Value path). */
1401
- function runnerResolveRef(priorNodeAt, atPos, typedNodes, plan, inputPorts) {
1402
- return (head, restPath, opt) => {
1403
- if (opt)
1404
- return null;
1405
- if (priorNodeAt(head, atPos)) {
1406
- const baseRef = typedNodes.get(head);
1407
- let acc;
1408
- let expr;
1409
- try {
1410
- const r = typedFieldAccess(`${typedCell(head)}.borrow()`, baseRef, restPath, plan);
1411
- expr = r.expr;
1412
- acc = r.ref;
1413
- }
1414
- catch {
1415
- return null;
1416
- }
1417
- if (acc.kind !== "scalar" || acc.scalar === "null")
1418
- return null;
1419
- // OWNED: a string field is cloned; int/float/bool are Copy (deref off the Ref).
1420
- const owned = acc.scalar === "string" ? `(${expr}).clone()` : `*(${expr})`;
1421
- return { expr: owned, scalar: rustScalar(acc.scalar) };
1422
- }
1423
- if (restPath.length !== 0)
1424
- return null;
1425
- const scalar = inputScalarKind(inputPorts?.[head]);
1426
- if (scalar === undefined)
1427
- return null;
1428
- const field = `in_.${rustFieldName(head)}`;
1429
- const owned = scalar === "String" ? `${field}.clone()` : field;
1430
- return { expr: owned, scalar };
1431
- };
1432
- }
1433
1018
  function rustScalar(s) {
1434
1019
  return s === "string" ? "String" : s === "int" ? "i64" : s === "float" ? "f64" : "bool";
1435
1020
  }
@@ -1621,12 +1206,9 @@ function emitNativeRawRunner(comp, plan, ap) {
1621
1206
  indent += " ";
1622
1207
  }
1623
1208
  }
1624
- const resolveRef = runnerResolveRef(priorNodeAt, k, typedNodes, plan, comp.inputPorts);
1625
1209
  const inits = [];
1626
1210
  for (const pn of portNames) {
1627
- const expr = node.ports[pn];
1628
- const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
1629
- 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)));
1630
1212
  }
1631
1213
  const boundArg = node.bindField !== undefined ? `bound_${s}` : "None";
1632
1214
  // bc#97: derived per-node await — this point-read's future is awaited HERE (its result-consumption
@@ -1754,12 +1336,9 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1754
1336
  ind += " ";
1755
1337
  }
1756
1338
  }
1757
- const resolveRef = runnerResolveRef(priorNodeAt, atPos, typedNodes, plan, comp.inputPorts);
1758
1339
  const inits = [];
1759
1340
  for (const pn of Object.keys(node.ports)) {
1760
- const expr = node.ports[pn];
1761
- const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
1762
- 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)));
1763
1342
  }
1764
1343
  if (gated) {
1765
1344
  lines.push(`${ind}ports_${s} = Some(${structName} { ${inits.join(", ")} });`);
@@ -1870,78 +1449,18 @@ isAsync = false) {
1870
1449
  const portNames = Object.keys(m.ports);
1871
1450
  const elemArrRef = typedNodes.get(node.id);
1872
1451
  const elemName = renderTypeRef(elemArrRef.elem);
1873
- const resolveRef = (head, restPath, opt) => {
1874
- if (opt)
1875
- return null;
1876
- let baseExpr;
1877
- let baseRef;
1878
- if (head === asName) {
1879
- baseExpr = `oel_${s}`;
1880
- baseRef = overElemRef;
1881
- }
1882
- else if (priorHere(head)) {
1883
- baseExpr = `${typedCell(head)}.borrow()`;
1884
- baseRef = typedNodes.get(head);
1885
- }
1886
- else {
1887
- if (restPath.length !== 0)
1888
- return null;
1889
- const scalar = inputScalarKind(comp.inputPorts?.[head]);
1890
- if (scalar === undefined)
1891
- return null;
1892
- const field = `in_.${rustFieldName(head)}`;
1893
- return { expr: scalar === "String" ? `${field}.clone()` : field, scalar };
1894
- }
1895
- let acc;
1896
- let expr;
1897
- try {
1898
- const r = typedFieldAccess(baseExpr, baseRef, restPath, plan);
1899
- expr = r.expr;
1900
- acc = r.ref;
1901
- }
1902
- catch {
1903
- return null;
1904
- }
1905
- if (acc.kind !== "scalar" || acc.scalar === "null")
1906
- return null;
1907
- // OWNED value. When restPath is EMPTY, `expr` is a bare REFERENCE (`oel_${s}` = &Elem-scalar, or
1908
- // `cell.borrow()` = Ref<scalar>) — deref with `*` (Copy) / `.clone()` via deref (String). When a
1909
- // `.field` was appended, `expr` is a value PLACE (auto-deref) — read it directly (`.clone()` String).
1910
- const isBareRef = restPath.length === 0;
1911
- let owned;
1912
- if (acc.scalar === "string") {
1913
- owned = isBareRef ? `(*${expr}).clone()` : `${expr}.clone()`;
1914
- }
1915
- else {
1916
- owned = isBareRef ? `*${expr}` : expr;
1917
- }
1918
- return { expr: owned, scalar: rustScalar(acc.scalar) };
1919
- };
1452
+ const asBinding = { name: asName, ref: overElemRef, plan };
1920
1453
  // build the per-element CONCRETE native ports struct. `il` = indent inside the element loop.
1921
1454
  const buildPorts = (il) => {
1922
- const out = [];
1923
- const inits = [];
1924
- const asBinding = { name: asName, ref: overElemRef, plan };
1925
- for (const pn of portNames) {
1926
- const ty = portFieldRustType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding, plan);
1927
- // #108-map: pass the element binding + base expr so a `$as` MAP/NAMED field port clones the typed composite.
1928
- inits.push(emitPortInit(pn, m.ports[pn], ty, resolveRef, comp.inputPorts, plan, undefined, asBinding, `oel_${s}`));
1929
- }
1930
- out.push(`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`);
1931
- 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(", ")} };`];
1932
1457
  };
1933
1458
  // #108 guard: compile `when` to a native bool over an element-scoped resolveHead ($as element value,
1934
1459
  // prior node, input port). On skip: `continue` (element excluded — matches run_behavior's keep-gate).
1935
1460
  const emitGuard = (il) => {
1936
1461
  if (!hasGuard)
1937
1462
  return [];
1938
- const resolveHead = (head) => {
1939
- if (head === asName)
1940
- return { expr: `oel_${s}.clone()`, ref: overElemRef };
1941
- if (priorHere(head))
1942
- return { expr: `${typedCell(head)}.borrow().clone()`, ref: typedNodes.get(head) };
1943
- return rustInputHeadRef(head, comp, plan);
1944
- };
1463
+ const resolveHead = nativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` });
1945
1464
  const g = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compileBool(m.when);
1946
1465
  const out = [];
1947
1466
  for (const st of g.stmts)
@@ -2064,55 +1583,9 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
2064
1583
  if (!dkField || dkField.type.kind !== "scalar" || dkField.type.scalar !== "string")
2065
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}'.`);
2066
1585
  const dkRust = rustFieldName(f.dedupeKey);
2067
- const resolveRef = (head, restPath, opt) => {
2068
- if (opt)
2069
- return null;
2070
- let baseExpr;
2071
- let baseRef;
2072
- if (head === asName) {
2073
- baseExpr = `oel_${s}`;
2074
- baseRef = overElemRef;
2075
- }
2076
- else if (priorHere(head)) {
2077
- baseExpr = `${typedCell(head)}.borrow()`;
2078
- baseRef = typedNodes.get(head);
2079
- }
2080
- else {
2081
- if (restPath.length !== 0)
2082
- return null;
2083
- const scalar = inputScalarKind(comp.inputPorts?.[head]);
2084
- if (scalar === undefined)
2085
- return null;
2086
- const field = `in_.${rustFieldName(head)}`;
2087
- return { expr: scalar === "String" ? `${field}.clone()` : field, scalar };
2088
- }
2089
- let acc;
2090
- let expr;
2091
- try {
2092
- const r = typedFieldAccess(baseExpr, baseRef, restPath, plan);
2093
- expr = r.expr;
2094
- acc = r.ref;
2095
- }
2096
- catch {
2097
- return null;
2098
- }
2099
- if (acc.kind !== "scalar" || acc.scalar === "null")
2100
- return null;
2101
- const isBareRef = restPath.length === 0;
2102
- let owned;
2103
- if (acc.scalar === "string")
2104
- owned = isBareRef ? `(*${expr}).clone()` : `${expr}.clone()`;
2105
- else
2106
- owned = isBareRef ? `*${expr}` : expr;
2107
- return { expr: owned, scalar: rustScalar(acc.scalar) };
2108
- };
1586
+ const asBinding = { name: asName, ref: overElemRef, plan };
2109
1587
  const buildPorts = (il) => {
2110
- const inits = [];
2111
- const asBinding = { name: asName, ref: overElemRef, plan };
2112
- for (const pn of portNames) {
2113
- const ty = portFieldRustType(f.ports[pn], comp.inputPorts, `fanout port '${pn}' of node '${node.id}'`, asBinding, plan);
2114
- inits.push(emitPortInit(pn, f.ports[pn], ty, resolveRef, comp.inputPorts, plan));
2115
- }
1588
+ const inits = portNames.map((pn) => emitPortInit(pn, f.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
2116
1589
  return [`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`];
2117
1590
  };
2118
1591
  const lines = [];
@@ -2167,28 +1640,71 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
2167
1640
  return lines.join("\n");
2168
1641
  }
2169
1642
  /**
2170
- * rustInputHeadRef (#108) resolve an INPUT port head for a native-expr ref (rust). The port's declared
2171
- * type comes from the shared `inputPortTypeRef` SSoT, so an OPTIONAL port resolves as `{opt:…}` (native
2172
- * `Option<T>`) and the compiler can see its optionality: a null-compare becomes a real presence test and
2173
- * a coalesce a real `unwrap_or`, while a REQUIRED-scalar slot fed by an opt fails closed. Exprs are OWNED
2174
- * (String / Option<String> cloned; Copy scalars as-is), except an array the only array use inside a
2175
- * cond/guard expr is `len`, which BORROWS (`&in_.xs`).
1643
+ * rustOwnrender 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.
2176
1649
  */
2177
- function rustInputHeadRef(head, comp, plan) {
2178
- const schema = comp.inputPorts?.[head];
2179
- const ref = inputPortTypeRef(schema, plan);
2180
- if (ref === undefined)
2181
- return null;
2182
- const field = `in_.${rustFieldName(head)}`;
2183
- return { expr: rustOwnedHeadExpr(field, ref), ref };
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
+ };
2184
1658
  }
2185
- /** The OWNED native expression for a head of type `ref` read off a by-value struct field. A String (or an
2186
- * Option/collection containing one) is cloned; a Copy scalar (and an Option/array of Copy scalars) is
2187
- * read directly cloning a Copy value trips clippy's `clone_on_copy`. An array stays borrowed (`len`). */
2188
- function rustOwnedHeadExpr(field, ref) {
2189
- if (ref.kind === "arr")
2190
- return field; // borrowed by `len`
2191
- return typeRefIsCopy(ref) ? field : `${field}.clone()`;
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
+ }
2192
1708
  }
2193
1709
  /**
2194
1710
  * emitCondArm — the struct-native exec of a covered cond node (#108, rust). Mirrors run_behavior's cond
@@ -2200,11 +1716,7 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
2200
1716
  const c = node.cond;
2201
1717
  const s = sanitize(node.id);
2202
1718
  const priorHere = (head) => priorNodeAt(head, atPos);
2203
- const resolveHead = (head) => {
2204
- if (priorHere(head))
2205
- return { expr: `${typedCell(head)}.borrow().clone()`, ref: typedNodes.get(head) };
2206
- return rustInputHeadRef(head, comp, plan);
2207
- };
1719
+ const resolveHead = nativeResolveHead(comp, plan, typedNodes, priorHere);
2208
1720
  const compiler = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan));
2209
1721
  const cond = compiler.compileBool(c.if);
2210
1722
  // #125: a when(...) control-gate cond は出力データ型を持たない制御スキップマーカー。typed cell へ何も
@@ -2343,17 +1855,20 @@ function emit(ctx) {
2343
1855
  // rust CONSUMES this annotation (async fn + .await where derived); go ignores it. per-node granularity
2344
1856
  // is a strict generalization of increment-1's uniform ioModel:'async' (= every terminal async).
2345
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);
2346
1861
  for (const c of ctx.ir.components)
2347
- assertTypedCoverage(c);
2348
- const native = ctx.ir.components.filter(isNativeRaw);
2349
- 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));
2350
1865
  // FAIL CLOSED (bc#86/#89): the --typed-native (native codegen) surface is native-only. Previously a
2351
1866
  // non-covered component was SILENTLY DROPPED (emitted only as a comment note), concealing that it
2352
1867
  // never got native codegen. We now throw a LOUD GeneratorFailure naming every uncovered component +
2353
1868
  // its specific reject reason. A fully-covered module is unchanged.
2354
1869
  if (nonNative.length > 0) {
2355
1870
  const rejects = nonNative
2356
- .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"}`)
2357
1872
  .join("\n");
2358
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` +
2359
1874
  `The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
@@ -2367,7 +1882,6 @@ function emit(ctx) {
2367
1882
  ${emitStructSurface([])}
2368
1883
  `;
2369
1884
  }
2370
- const plan = buildTypePlan(ctx.ir);
2371
1885
  // The typed-native module is a de-boxed CONSUMER artifact: the covered runner RETURNS these outType
2372
1886
  // structs and the consumer reads their fields natively. So the decls (and their FIELDS) are pub — the
2373
1887
  // consumer keeps the model native (mirrors Go's exported outType struct fields on the covered path).
@@ -2385,11 +1899,7 @@ ${emitStructSurface([])}
2385
1899
  const inStructs = [];
2386
1900
  const elemMaterializers = [];
2387
1901
  for (const c of native) {
2388
- const typedNodes = new Map();
2389
- // build the full typed-node map FIRST so a map's over-element resolution can see every node.
2390
- for (const n of c.body)
2391
- if (!isControlGate(n))
2392
- 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);
2393
1903
  for (const n of c.body) {
2394
1904
  if ("fanout" in n)
2395
1905
  structs.push(emitFanoutPortsStructs(c, n, typedNodes, plan));
@@ -2399,7 +1909,7 @@ ${emitStructSurface([])}
2399
1909
  // #108: a cond node has NO ports struct (pure Expression — no handler dispatch).
2400
1910
  }
2401
1911
  else
2402
- 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.
2403
1913
  }
2404
1914
  rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
2405
1915
  handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
@@ -2464,8 +1974,8 @@ use std::cell::RefCell;`;
2464
1974
  */
2465
1975
  export function rustTypedNativeObserve(ir, ioModel = "sync") {
2466
1976
  const components = ir.components;
2467
- const native = components.filter(isNativeRaw);
2468
1977
  const plan = buildTypePlan(ir);
1978
+ const native = components.filter((c) => isNativeRaw(c, plan));
2469
1979
  const needSync = native.some((c) => concurrencyPlan(c) !== null);
2470
1980
  const serializers = emitSerializers(plan);
2471
1981
  const marshallers = emitMarshallers(plan);
@@ -2573,10 +2083,15 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
2573
2083
  // bound only when at least one port qualifies.
2574
2084
  const echoable = echoablePorts(c, node, ref, plan, typedNodes);
2575
2085
  const portsParam = echoable.length > 0 ? "ports" : "_ports";
2576
- // a Copy port (i64/f64/bool/Option<scalar>) is read directly — cloning it trips clippy clone_on_copy.
2577
- const echoRead = (p) => `${portsParam}.${portFieldName(p)}${typeRefIsCopy(ref) ? "" : ".clone()"}`;
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() }`;
2578
2093
  const echoBranch = echoable
2579
- .map((p) => `\n if echo == ${rustStrLit(p)} {\n return Some(${rowT} { val: ${echoRead(p)}, ..Default::default() });\n }`)
2094
+ .map((p) => `\n if echo == ${rustStrLit(p)} {\n return Some(${echoRow(p)});\n }`)
2580
2095
  .join("");
2581
2096
  const okBind = echoable.length > 0 ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
2582
2097
  methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
@@ -2817,14 +2332,12 @@ function emitInDecoder(comp, plan) {
2817
2332
  return lines.join("\n");
2818
2333
  }
2819
2334
  /** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
2820
- * declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row's `val`. A
2821
- * port with a different type is not echoable (the row could not hold it), and a NAMED outType has no
2822
- * `val` at all (its row carries the struct's fields flattened see emitDecodeRow). */
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). */
2823
2338
  function echoablePorts(comp, node, ref, plan, typedNodes) {
2824
- if (ref.kind === "named")
2825
- return [];
2826
2339
  const want = renderTypeRef(ref);
2827
- return Object.keys(node.ports).filter((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}'`, undefined, plan, typedNodes) === want);
2340
+ return Object.keys(node.ports).filter((p) => portFieldRustType(node.ports[p], comp, plan, typedNodes, `port '${p}'`) === want);
2828
2341
  }
2829
2342
  /** emitDecodeRow — emit (once, deduped) a decoder `decode_row_<comp>_<node><suffix>(v: &Value) -> <rowT>`
2830
2343
  * that materializes the scripted ok Value into the concrete row struct. Named: reuse the Value->struct