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.
- package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-go.js +141 -616
- package/dist/generator/emit-typed-native-go.js.map +1 -1
- package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-rust.js +177 -664
- package/dist/generator/emit-typed-native-rust.js.map +1 -1
- package/dist/generator/native-expr.d.ts +23 -2
- package/dist/generator/native-expr.d.ts.map +1 -1
- package/dist/generator/native-expr.js +31 -2
- package/dist/generator/native-expr.js.map +1 -1
- package/package.json +1 -1
|
@@ -88,240 +88,15 @@ function staticArrElems(node) {
|
|
|
88
88
|
return Array.isArray(arg) ? arg : null;
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
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
|
+
* portFieldRustType — the NATIVE Rust type for a covered port field (bc#90 / runtime-free), read off the
|
|
92
|
+
* ONE port lowering (compilePortNode / native-expr). The covered ports struct carries NO `Value` field:
|
|
93
|
+
* every port lowers to a concrete Rust type, and a port that cannot lower FAILS CLOSED. The field type is
|
|
94
|
+
* the compiled expression's TypeRef, so it can never disagree with the initializer (emitPortInit), which
|
|
95
|
+
* is the SAME lowering read for its value. `where` names the emission site for the error.
|
|
176
96
|
*/
|
|
177
|
-
function portFieldRustType(node,
|
|
178
|
-
|
|
179
|
-
|
|
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,
|
|
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
|
|
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 },
|
|
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 },
|
|
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
|
-
*
|
|
929
|
-
*
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1006
|
+
* emitPortInit — the port field initializer, read off the ONE port lowering (compilePortNode / native-
|
|
1007
|
+
* expr). The field's declared TYPE (portFieldRustType) is the SAME lowering's TypeRef, so type and value
|
|
1008
|
+
* can never disagree. `isPrior` tests whether a ref head names a settled prior node at this position;
|
|
1009
|
+
* `opts.asBinding`/`opts.asBase` carry a map/fanout element binding.
|
|
1273
1010
|
*/
|
|
1274
|
-
function
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1923
|
-
|
|
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 = (
|
|
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
|
|
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
|
-
*
|
|
2171
|
-
*
|
|
2172
|
-
* `
|
|
2173
|
-
*
|
|
2174
|
-
*
|
|
2175
|
-
*
|
|
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.
|
|
2176
1649
|
*/
|
|
2177
|
-
function
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
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
|
-
/**
|
|
2186
|
-
*
|
|
2187
|
-
*
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
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 = (
|
|
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 =
|
|
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,
|
|
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
|
|
2577
|
-
const
|
|
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(${
|
|
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
|
|
2821
|
-
*
|
|
2822
|
-
*
|
|
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
|
|
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
|