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