behavior-contracts 0.8.6 → 0.8.8
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/cli-contract.yaml +189 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +76 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/codegen.d.ts +6 -0
- package/dist/commands/codegen.d.ts.map +1 -0
- package/dist/commands/codegen.js +39 -0
- package/dist/commands/codegen.js.map +1 -0
- package/dist/generated/commands.d.ts +8 -0
- package/dist/generated/commands.d.ts.map +1 -0
- package/dist/generated/commands.js +55 -0
- package/dist/generated/commands.js.map +1 -0
- package/dist/generated/contract.d.ts +3 -0
- package/dist/generated/contract.d.ts.map +1 -0
- package/dist/generated/contract.js +5 -0
- package/dist/generated/contract.js.map +1 -0
- package/dist/generated/index.d.ts +7 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/index.js +7 -0
- package/dist/generated/index.js.map +1 -0
- package/dist/generated/program.d.ts +17 -0
- package/dist/generated/program.d.ts.map +1 -0
- package/dist/generated/program.js +117 -0
- package/dist/generated/program.js.map +1 -0
- package/dist/generated/schemas.d.ts +2 -0
- package/dist/generated/schemas.d.ts.map +1 -0
- package/dist/generated/schemas.js +3 -0
- package/dist/generated/schemas.js.map +1 -0
- package/dist/generated/types.d.ts +35 -0
- package/dist/generated/types.d.ts.map +1 -0
- package/dist/generated/types.js +3 -0
- package/dist/generated/types.js.map +1 -0
- package/dist/generator/core.d.ts +28 -0
- package/dist/generator/core.d.ts.map +1 -1
- package/dist/generator/core.js +7 -0
- package/dist/generator/core.js.map +1 -1
- package/dist/generator/emit-shared-debox.d.ts +108 -0
- package/dist/generator/emit-shared-debox.d.ts.map +1 -0
- package/dist/generator/emit-shared-debox.js +117 -0
- package/dist/generator/emit-shared-debox.js.map +1 -0
- package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-go.js +436 -1078
- package/dist/generator/emit-typed-native-go.js.map +1 -1
- package/dist/generator/emit-typed-native-rust.d.ts +9 -7
- package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-rust.js +313 -844
- package/dist/generator/emit-typed-native-rust.js.map +1 -1
- package/dist/generator/typed.d.ts +4 -2
- package/dist/generator/typed.d.ts.map +1 -1
- package/dist/generator/typed.js +4 -2
- package/dist/generator/typed.js.map +1 -1
- package/package.json +12 -2
|
@@ -6,6 +6,7 @@ import { buildTypePlan, deriveTypeRef, findDecl, inputPortIsOptional, inputPortT
|
|
|
6
6
|
import { concurrencyPlan, execPlan, analyzeSequentialOps, classifyExpr } from "./straightline.js";
|
|
7
7
|
import { NativeExprCompiler } from "./native-expr.js";
|
|
8
8
|
import { buildAsyncPlans } from "./async-plan.js";
|
|
9
|
+
import { buildDeBoxPlan, notationOfRef } from "./emit-shared-debox.js";
|
|
9
10
|
const { sanitize, rustStrLit } = rustStraightlineInternals;
|
|
10
11
|
const { typeRefIsCopy, rustFieldName, renderTypeRef, typedCell, typedFieldAccess, serializeTyped, emitTypeDecls, emitDefaults, emitSerializers, emitTypedValue, emitMarshallers, emitMarshalHelpers, materializeExpr, } = rustTypedInternals;
|
|
11
12
|
/** rustErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
|
|
@@ -172,14 +173,6 @@ function pascal(s) {
|
|
|
172
173
|
function portsStructName(compName, nodeId) {
|
|
173
174
|
return `PortsNR${pascal(compName)}${pascal(nodeId)}`;
|
|
174
175
|
}
|
|
175
|
-
/** RawRowNR<Comp><Node> — the concrete per-node handler-result row struct name. */
|
|
176
|
-
function rawRowStructName(compName, nodeId) {
|
|
177
|
-
return `RawRowNR${pascal(compName)}${pascal(nodeId)}`;
|
|
178
|
-
}
|
|
179
|
-
/** RawElemNR<Comp><Node> — the concrete per-ELEMENT row struct name for a covered map node. */
|
|
180
|
-
function rawElemStructName(compName, nodeId) {
|
|
181
|
-
return `RawElemNR${pascal(compName)}${pascal(nodeId)}`;
|
|
182
|
-
}
|
|
183
176
|
/** HandlerNR<Comp> — the per-component concrete handler trait name (one node_* method per node). */
|
|
184
177
|
function handlerTraitName(compName) {
|
|
185
178
|
return `HandlerNR${pascal(compName)}`;
|
|
@@ -192,56 +185,17 @@ function nodeMethodName(nodeId) {
|
|
|
192
185
|
function inStructName(compName) {
|
|
193
186
|
return `InNR${pascal(compName)}`;
|
|
194
187
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
/** deBoxEligible — a componentRef node whose result is a record-containing top (named / arr(named) /
|
|
207
|
-
* opt(named) / map(named)). Its handler returns the wire; the generated decode probes each record. */
|
|
208
|
-
function deBoxEligible(ref) {
|
|
209
|
-
return recordTop(ref) !== null;
|
|
210
|
-
}
|
|
211
|
-
/** rustWireTypeForTop — the Rust handler return type (over the associated Self::Wire) for a record top. */
|
|
212
|
-
function rustWireTypeForTop(top) {
|
|
213
|
-
switch (top.kind) {
|
|
214
|
-
case "named":
|
|
215
|
-
return "Self::Wire";
|
|
216
|
-
case "arr":
|
|
217
|
-
return "Vec<Self::Wire>";
|
|
218
|
-
case "opt":
|
|
219
|
-
return "Option<Self::Wire>";
|
|
220
|
-
case "map":
|
|
221
|
-
return "std::collections::BTreeMap<String, Self::Wire>";
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
/** notationOfRef — the Portable Type Notation of a TypeRef (scp-error.md), resolving a named ref to its
|
|
225
|
-
* obj notation via the plan. Baked as the `expectedType` literal (a rendering of the STATICALLY declared
|
|
226
|
-
* type — nothing walks a type at runtime). */
|
|
227
|
-
function notationOfRef(ref, plan) {
|
|
228
|
-
switch (ref.kind) {
|
|
229
|
-
case "scalar":
|
|
230
|
-
return ref.scalar;
|
|
231
|
-
case "opt":
|
|
232
|
-
return `opt(${notationOfRef(ref.inner, plan)})`;
|
|
233
|
-
case "arr":
|
|
234
|
-
return `arr(${notationOfRef(ref.elem, plan)})`;
|
|
235
|
-
case "map":
|
|
236
|
-
return `map(${notationOfRef(ref.value, plan)})`;
|
|
237
|
-
case "named": {
|
|
238
|
-
const decl = findDecl(plan, ref.name);
|
|
239
|
-
return `obj{${decl.fields.map((f) => `${f.name}:${notationOfRef(f.type, plan)}`).join(",")}}`;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
/** emitRustProbeWireTypes — the once-per-module probe enums + WireRow/WireList seam traits. Concrete
|
|
244
|
-
* enums; monomorphized traits (mutual Row/List associated types keep nesting concrete, no dyn/Box). */
|
|
188
|
+
// ── strict de-box: the probe/wire seam + generated decode orchestration ───────────────────────
|
|
189
|
+
//
|
|
190
|
+
// A covered record read returns the consumer's WIRE (a value implementing WireRow), and the generated
|
|
191
|
+
// decode probes it per field from the declared type. The consumer supplies variant classification (a
|
|
192
|
+
// probe reports the producer's own wire tag as a free string); the generated code owns strictness —
|
|
193
|
+
// required/optional, present/absent, error assembly, fail-closed. The seam stays native: Probe/NumProbe
|
|
194
|
+
// are concrete enums and WireRow/WireList are monomorphized via mutual associated types (Row/List), so no
|
|
195
|
+
// boxed Value / dyn / Box crosses the boundary.
|
|
196
|
+
/** emitRustProbeWireTypes — the once-per-module probe enums + WireValue/WireRow/WireList seam traits.
|
|
197
|
+
* Concrete enums; monomorphized traits (WireValue's Row associated type threads WireRow, whose List
|
|
198
|
+
* threads WireList — nesting stays concrete, no dyn/Box). */
|
|
245
199
|
function emitRustProbeWireTypes() {
|
|
246
200
|
return `// Probe<T> / NumProbe — the outcome of classifying one wire attribute against a declared type. Got carries
|
|
247
201
|
// the matched value (a NumProbe's raw numeric text, which the de-box parses + range-checks so overflow is
|
|
@@ -261,6 +215,20 @@ pub enum NumProbe {
|
|
|
261
215
|
Absent,
|
|
262
216
|
}
|
|
263
217
|
|
|
218
|
+
// WireValue — a node result's own wire datum (the uniform handler return): the producer's value at a
|
|
219
|
+
// result-consumption point. The generated de-box classifies it against the STATICALLY declared node type
|
|
220
|
+
// via these inherent methods (as_row / as_list yield the WireRow / WireList the nested decode then probes).
|
|
221
|
+
// A top value is always present, so there is no Absent — a producer-null is Null. Monomorphized (Row
|
|
222
|
+
// associated type threads WireRow) — no dyn, no Box, no boxed value crosses the seam.
|
|
223
|
+
pub trait WireValue: Sized {
|
|
224
|
+
type Row: WireRow;
|
|
225
|
+
fn as_string(&self) -> Probe<String>;
|
|
226
|
+
fn as_number(&self) -> NumProbe;
|
|
227
|
+
fn as_bool(&self) -> Probe<bool>;
|
|
228
|
+
fn as_row(&self) -> Probe<Self::Row>;
|
|
229
|
+
fn as_list(&self) -> Probe<<Self::Row as WireRow>::List>;
|
|
230
|
+
}
|
|
231
|
+
|
|
264
232
|
// WireRow / WireList — the consumer's wire item / wire array, probed per declared field / element by the
|
|
265
233
|
// generated de-box. The consumer implements them over its wire payload (a DynamoDB AttributeValue map)
|
|
266
234
|
// and classifies each attribute's variant; the de-box owns strictness. Monomorphized (Row/List mutual
|
|
@@ -289,147 +257,124 @@ pub trait WireList: Sized {
|
|
|
289
257
|
fn elem_list(&self, i: usize) -> Probe<Self>;
|
|
290
258
|
}`;
|
|
291
259
|
}
|
|
292
|
-
/** rustDecodeNamedName — the decode function name for a named type (deduped across the module). */
|
|
293
|
-
function rustDecodeNamedName(name) {
|
|
294
|
-
return `decode_wire_${name.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase()}`;
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* emitRustDecodeNamed — emit (once, deduped) `decode_wire_<name><W: WireRow>(w: &W) -> Result<Name,
|
|
298
|
-
* BehaviorError>`: probe each declared field from the type SSoT (via the single rustDecodeValue
|
|
299
|
-
* recursion) and build the struct, failing closed with the structured Error Value on a mismatch.
|
|
300
|
-
*/
|
|
301
|
-
function emitRustDecodeNamed(name, plan, emitted, out) {
|
|
302
|
-
const fn = rustDecodeNamedName(name);
|
|
303
|
-
if (emitted.has(fn))
|
|
304
|
-
return fn;
|
|
305
|
-
emitted.add(fn);
|
|
306
|
-
const decl = findDecl(plan, name);
|
|
307
|
-
const lines = [];
|
|
308
|
-
lines.push(`// ${fn} — the strict de-box for '${name}': probe each declared field, fail closed on a`);
|
|
309
|
-
lines.push(`// wire mismatch with the structured Error Value (byte-equal codes to run_behavior).`);
|
|
310
|
-
lines.push(`fn ${fn}<W: WireRow>(w: &W) -> Result<${name}, BehaviorError> {`);
|
|
311
|
-
const ctr = { n: 0 };
|
|
312
|
-
const binds = [];
|
|
313
|
-
for (const f of decl.fields) {
|
|
314
|
-
const rf = rustFieldName(f.name);
|
|
315
|
-
const expr = rustDecodeValue(f.type, "w", rustStrLit(f.name), "row", name, f.name, plan, emitted, out, ctr, 1);
|
|
316
|
-
lines.push(` let ${rf} = ${expr};`);
|
|
317
|
-
binds.push(rf);
|
|
318
|
-
}
|
|
319
|
-
lines.push(` Ok(${name} { ${binds.join(", ")} })`);
|
|
320
|
-
lines.push(`}`);
|
|
321
|
-
out.push(lines.join("\n"));
|
|
322
|
-
return fn;
|
|
323
|
-
}
|
|
324
260
|
/**
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
261
|
+
* renderRustDeBox — the ONE inline, recursive strict de-box renderer. Folds a language-neutral
|
|
262
|
+
* {@link DeBoxPlan} (the SSoT walk) into a single Rust EXPRESSION that evaluates to the decoded native
|
|
263
|
+
* value of the plan's type, obtained by probing `srcExpr` per the node's access (top → `as_*`, field →
|
|
264
|
+
* `probe_*(key)`, elem → `elem_*(i)`, mapval → `probe_*(&k)`). The recursion runs HERE at generation
|
|
265
|
+
* time (walking the plan), so the emitted Rust is FLAT, CONCRETE, fully-unrolled code: NO `decode_wire_*`
|
|
266
|
+
* helper function, NO output-side recursion — a nested record expands to a nested struct literal, a
|
|
267
|
+
* nested collection to a nested loop. A required-position mismatch does `return Err(de_*(...))` (fail
|
|
268
|
+
* closed, propagating the structured Error Value from the enclosing runner); an optional position
|
|
269
|
+
* (optDepth>0) maps absent/null to the all-None value. `ctr` hands out unique temp names (`sub2`, `l3`,
|
|
270
|
+
* `m4`, `i3`, `k4`) so nested blocks never collide. `idxExpr` is the element index / map-key argument the
|
|
271
|
+
* enclosing list / map loop supplies (empty for top / field). Mirrors the interpreter's outType check +
|
|
272
|
+
* the (now-deleted) per-position decoders, but TOTAL over the grammar (opt(named)/arr(arr)/map(opt)…) and
|
|
273
|
+
* inlined — the type is baked all the way through by the TS compiler, not the target monomorphizer.
|
|
331
274
|
*/
|
|
332
|
-
function
|
|
333
|
-
const I =
|
|
334
|
-
const I1 =
|
|
335
|
-
const ml = rustStrLit(model);
|
|
336
|
-
const fl = rustStrLit(fieldRaw);
|
|
337
|
-
const nl = rustStrLit(notationOfRef(ref, plan));
|
|
275
|
+
function renderRustDeBox(plan, srcExpr, ctr, indent, fail, idxExpr = "") {
|
|
276
|
+
const I = indent;
|
|
277
|
+
const I1 = indent + " ";
|
|
338
278
|
const id = ctr.n++;
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
${
|
|
279
|
+
const ml = rustStrLit(plan.model);
|
|
280
|
+
const fl = rustStrLit(plan.field);
|
|
281
|
+
const nl = rustStrLit(plan.expectedType);
|
|
282
|
+
const opt = plan.optDepth > 0;
|
|
283
|
+
const mismatch = fail(`de_type_mismatch(${ml}, ${fl}, ${nl}, actual_wire_type, raw_value)`);
|
|
284
|
+
const missing = fail(`de_missing_field(${ml}, ${fl}, ${nl})`);
|
|
285
|
+
// the probe expression for this node's access + a value-kind suffix (string/number/bool/row/list).
|
|
286
|
+
const probe = (suffix) => {
|
|
287
|
+
switch (plan.access.from) {
|
|
288
|
+
case "top":
|
|
289
|
+
return `${srcExpr}.as_${suffix}()`;
|
|
290
|
+
case "field":
|
|
291
|
+
return `${srcExpr}.probe_${suffix}(${rustStrLit(plan.access.key)})`;
|
|
292
|
+
case "elem":
|
|
293
|
+
return `${srcExpr}.elem_${suffix}(${idxExpr})`;
|
|
294
|
+
case "mapval":
|
|
295
|
+
return `${srcExpr}.probe_${suffix}(${idxExpr})`;
|
|
354
296
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
${
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
297
|
+
};
|
|
298
|
+
// wrap the decoded value in optDepth `Some(...)` layers (present ⇒ Some^N(v); absent/null ⇒ None).
|
|
299
|
+
const wrapSome = (expr) => {
|
|
300
|
+
let e = expr;
|
|
301
|
+
for (let i = 0; i < plan.optDepth; i++)
|
|
302
|
+
e = `Some(${e})`;
|
|
303
|
+
return e;
|
|
304
|
+
};
|
|
305
|
+
// the three non-Got Probe<T> arms (scalar string/bool, record, list, map): required ⇒ wrong/null
|
|
306
|
+
// mismatch + absent missing; optional ⇒ wrong mismatch, null/absent None.
|
|
307
|
+
const probeArms = opt
|
|
308
|
+
? `${I1}Probe::Wrong { actual_wire_type, raw_value } => ${mismatch},\n${I1}Probe::Null { .. } | Probe::Absent => None,`
|
|
309
|
+
: `${I1}Probe::Wrong { actual_wire_type, raw_value }\n${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},\n${I1}Probe::Absent => ${missing},`;
|
|
310
|
+
if (plan.k === "scalar") {
|
|
311
|
+
if (plan.scalar === "int" || plan.scalar === "float") {
|
|
312
|
+
const rustT = plan.scalar === "int" ? "i64" : "f64";
|
|
313
|
+
const numArms = opt
|
|
314
|
+
? `${I1}NumProbe::Wrong { actual_wire_type, raw_value } => ${mismatch},\n${I1}NumProbe::Null { .. } | NumProbe::Absent => None,`
|
|
315
|
+
: `${I1}NumProbe::Wrong { actual_wire_type, raw_value }\n${I1}| NumProbe::Null { actual_wire_type, raw_value } => ${mismatch},\n${I1}NumProbe::Absent => ${missing},`;
|
|
316
|
+
return `match ${probe("number")} {
|
|
366
317
|
${I1}NumProbe::Got { raw, actual_wire_type } => match raw.parse::<${rustT}>() {
|
|
367
|
-
${I1} Ok(n) => n,
|
|
368
|
-
${I1} Err(_) =>
|
|
318
|
+
${I1} Ok(n) => ${wrapSome("n")},
|
|
319
|
+
${I1} Err(_) => ${fail(`de_overflow(${ml}, ${fl}, ${nl}, actual_wire_type, raw)`)},
|
|
369
320
|
${I1}},
|
|
370
|
-
${
|
|
371
|
-
${I1}| NumProbe::Null { actual_wire_type, raw_value } => ${mismatch},
|
|
372
|
-
${I1}NumProbe::Absent => ${missing},
|
|
321
|
+
${numArms}
|
|
373
322
|
${I}}`;
|
|
374
323
|
}
|
|
375
|
-
const meth =
|
|
376
|
-
return `match ${
|
|
377
|
-
${I1}Probe::Got(v) => v,
|
|
378
|
-
${
|
|
379
|
-
${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
|
|
380
|
-
${I1}Probe::Absent => ${missing},
|
|
324
|
+
const meth = plan.scalar === "string" ? "string" : "bool";
|
|
325
|
+
return `match ${probe(meth)} {
|
|
326
|
+
${I1}Probe::Got(v) => ${wrapSome("v")},
|
|
327
|
+
${probeArms}
|
|
381
328
|
${I}}`;
|
|
382
329
|
}
|
|
383
|
-
if (
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
${
|
|
388
|
-
${I1}Probe::
|
|
389
|
-
${
|
|
390
|
-
${I1}
|
|
330
|
+
if (plan.k === "record") {
|
|
331
|
+
const fieldInits = plan.fields
|
|
332
|
+
.map((f) => `${I1} ${rustFieldName(f.name)}: ${renderRustDeBox(f.plan, `sub${id}`, ctr, `${I1} `, fail)},`)
|
|
333
|
+
.join("\n");
|
|
334
|
+
return `match ${probe("row")} {
|
|
335
|
+
${I1}Probe::Got(sub${id}) => ${wrapSome(`${plan.typeName} {
|
|
336
|
+
${fieldInits}
|
|
337
|
+
${I1}}`)},
|
|
338
|
+
${probeArms}
|
|
391
339
|
${I}}`;
|
|
392
340
|
}
|
|
393
|
-
if (
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
${I1}
|
|
398
|
-
${I1} let mut acc = Vec::with_capacity(l${id}.len());
|
|
341
|
+
if (plan.k === "list") {
|
|
342
|
+
const elemExpr = renderRustDeBox(plan.elem, `l${id}`, ctr, `${I1} `, fail, `i${id}`);
|
|
343
|
+
return `match ${probe("list")} {
|
|
344
|
+
${I1}Probe::Got(l${id}) => ${wrapSome(`{
|
|
345
|
+
${I1} let mut acc${id} = Vec::with_capacity(l${id}.len());
|
|
399
346
|
${I1} for i${id} in 0..l${id}.len() {
|
|
400
|
-
${I1} acc.push(${elemExpr});
|
|
347
|
+
${I1} acc${id}.push(${elemExpr});
|
|
401
348
|
${I1} }
|
|
402
|
-
${I1} acc
|
|
403
|
-
${I1}}
|
|
404
|
-
${
|
|
405
|
-
${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
|
|
406
|
-
${I1}Probe::Absent => ${missing},
|
|
349
|
+
${I1} acc${id}
|
|
350
|
+
${I1}}`)},
|
|
351
|
+
${probeArms}
|
|
407
352
|
${I}}`;
|
|
408
353
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return `match ${srcExpr}.${rowMeth}(${keyExpr}) {
|
|
415
|
-
${I1}Probe::Got(m${id}) => {
|
|
416
|
-
${I1} let mut acc = std::collections::BTreeMap::new();
|
|
354
|
+
// map(V): the wire value is a producer map (a DynamoDB "M"); enumerate its keys, decode each value as V.
|
|
355
|
+
const valExpr = renderRustDeBox(plan.value, `m${id}`, ctr, `${I1} `, fail, `&k${id}`);
|
|
356
|
+
return `match ${probe("row")} {
|
|
357
|
+
${I1}Probe::Got(m${id}) => ${wrapSome(`{
|
|
358
|
+
${I1} let mut acc${id} = std::collections::BTreeMap::new();
|
|
417
359
|
${I1} for k${id} in m${id}.keys() {
|
|
418
360
|
${I1} let v${id} = ${valExpr};
|
|
419
|
-
${I1} acc.insert(k${id}, v${id});
|
|
361
|
+
${I1} acc${id}.insert(k${id}, v${id});
|
|
420
362
|
${I1} }
|
|
421
|
-
${I1} acc
|
|
422
|
-
${I1}}
|
|
423
|
-
${
|
|
424
|
-
${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
|
|
425
|
-
${I1}Probe::Absent => ${missing},
|
|
363
|
+
${I1} acc${id}
|
|
364
|
+
${I1}}`)},
|
|
365
|
+
${probeArms}
|
|
426
366
|
${I}}`;
|
|
427
|
-
}
|
|
428
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: '${fieldRaw}' of '${model}' has type ${JSON.stringify(ref)} which the generated de-box does not decode (supported: scalar / opt(scalar) / named / arr / map).`);
|
|
429
367
|
}
|
|
430
|
-
/**
|
|
431
|
-
|
|
432
|
-
|
|
368
|
+
/** returnErrFail — the default de-box mismatch control flow: propagate the structured Error Value out of
|
|
369
|
+
* the enclosing runner (fail/retry componentRef results + map/fanout element de-box, which fails closed
|
|
370
|
+
* regardless of elementPolicy — matching the interpreter's whole-node outType throw). */
|
|
371
|
+
const returnErrFail = (errValueExpr) => `return Err(${errValueExpr})`;
|
|
372
|
+
/** renderResultDeBox — inline strict de-box of a whole result WIRE (`srcExpr`, a WireValue) into its
|
|
373
|
+
* native value, from the TOTAL SSoT plan for `ref` at the top position. `fail` selects the mismatch
|
|
374
|
+
* control flow. This is the ONE de-box call the runner arms share (componentRef result + map/fanout
|
|
375
|
+
* element) — no per-position predicate, no helper decode fn. */
|
|
376
|
+
function renderResultDeBox(ref, srcExpr, nodeId, plan, indent, fail = returnErrFail) {
|
|
377
|
+
return renderRustDeBox(buildDeBoxPlan(ref, { from: "top" }, { model: nodeId, field: nodeId, plan }), srcExpr, { n: 0 }, indent, fail);
|
|
433
378
|
}
|
|
434
379
|
/** mapFanoutElemRef — the element ROW TypeRef a map/fanout node decodes (the per-element record). */
|
|
435
380
|
function mapFanoutElemRef(node, typedNodes, plan) {
|
|
@@ -438,185 +383,6 @@ function mapFanoutElemRef(node, typedNodes, plan) {
|
|
|
438
383
|
return fanoutItemsElemRef(node, ref, plan);
|
|
439
384
|
return mapElemRowRef(node, ref, plan);
|
|
440
385
|
}
|
|
441
|
-
/** mapFanoutElemDeBoxEligible — a map/fanout node whose element ROW is a named record de-boxes each
|
|
442
|
-
* element through the generated decode_wire_elem_* (closing the scan-row silent-default hole). */
|
|
443
|
-
function mapFanoutElemDeBoxEligible(node, typedNodes, plan) {
|
|
444
|
-
return mapFanoutElemRef(node, typedNodes, plan).kind === "named";
|
|
445
|
-
}
|
|
446
|
-
/**
|
|
447
|
-
* emitRustDecodeElemRow — emit (once) `decode_wire_elem_<comp>_<node><W: WireRow>(w: &W) ->
|
|
448
|
-
* Result<RawElemNR<comp><node>, BehaviorError>` for a map/fanout node whose element ROW is a named
|
|
449
|
-
* record: probe each declared field via the SAME rustDecodeValue recursion as record reads and build the
|
|
450
|
-
* concrete RawElemNR struct, failing closed with the structured Error Value on a mismatch.
|
|
451
|
-
*/
|
|
452
|
-
function emitRustDecodeElemRow(comp, node, elemRowRef, plan, emitted, out) {
|
|
453
|
-
const nodeId = node.id;
|
|
454
|
-
const fn = rustDecodeElemName(comp.name, nodeId);
|
|
455
|
-
if (emitted.has(fn))
|
|
456
|
-
return fn;
|
|
457
|
-
emitted.add(fn);
|
|
458
|
-
const retType = rawElemStructName(comp.name, nodeId);
|
|
459
|
-
const decl = findDecl(plan, elemRowRef.name);
|
|
460
|
-
const lines = [];
|
|
461
|
-
lines.push(`// ${fn} — the strict de-box for a '${nodeId}' element row: probe each declared field, fail`);
|
|
462
|
-
lines.push(`// closed on a wire mismatch with the structured Error Value (byte-equal codes to run_behavior).`);
|
|
463
|
-
lines.push(`fn ${fn}<W: WireRow>(w: &W) -> Result<${retType}, BehaviorError> {`);
|
|
464
|
-
const ctr = { n: 0 };
|
|
465
|
-
const binds = [];
|
|
466
|
-
for (const f of decl.fields) {
|
|
467
|
-
const rf = rustFieldName(f.name);
|
|
468
|
-
const expr = rustDecodeValue(f.type, "w", rustStrLit(f.name), "row", elemRowRef.name, f.name, plan, emitted, out, ctr, 1);
|
|
469
|
-
lines.push(` let ${rf} = ${expr};`);
|
|
470
|
-
binds.push(rf);
|
|
471
|
-
}
|
|
472
|
-
lines.push(` Ok(${retType} { ${binds.join(", ")} })`);
|
|
473
|
-
lines.push(`}`);
|
|
474
|
-
out.push(lines.join("\n"));
|
|
475
|
-
return fn;
|
|
476
|
-
}
|
|
477
|
-
/** emitRustNodeDecodeFns — emit the decode functions for every de-box-eligible node: a componentRef
|
|
478
|
-
* record read (decode_wire_<name>) OR a map/fanout element row (decode_wire_elem_<comp>_<node>). */
|
|
479
|
-
/** rustDecodeResultName — the top-result decode function name for a de-box componentRef node. */
|
|
480
|
-
function rustDecodeResultName(compName, nodeId) {
|
|
481
|
-
return `decode_wire_result_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase()}`;
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* emitRustDecodeWireResult — emit (once) `decode_wire_result_<comp>_<node><W: WireRow>(wire: <WireArg>)
|
|
485
|
-
* -> Result<<ResultType>, BehaviorError>` for a componentRef node whose result is a record-containing
|
|
486
|
-
* top: it decodes each record via decode_wire_<name> and assembles the top (record / Vec / Option /
|
|
487
|
-
* BTreeMap). One path covers named / arr(named) / opt(named) / map(named).
|
|
488
|
-
*/
|
|
489
|
-
function emitRustDecodeWireResult(comp, node, ref, plan, emitted, out) {
|
|
490
|
-
const top = recordTop(ref);
|
|
491
|
-
const fn = rustDecodeResultName(comp.name, node.id);
|
|
492
|
-
if (emitted.has(fn))
|
|
493
|
-
return fn;
|
|
494
|
-
emitted.add(fn);
|
|
495
|
-
const innerDecode = emitRustDecodeNamed(top.innerName, plan, emitted, out);
|
|
496
|
-
const resultType = renderTypeRef(ref);
|
|
497
|
-
const lines = [];
|
|
498
|
-
lines.push(`// ${fn} — decode the node's record-containing '${top.kind}' result from the wire (each record`);
|
|
499
|
-
lines.push(`// via ${innerDecode}); a de-box mismatch fails closed with the structured Error Value.`);
|
|
500
|
-
switch (top.kind) {
|
|
501
|
-
case "named":
|
|
502
|
-
lines.push(`fn ${fn}<W: WireRow>(wire: W) -> Result<${resultType}, BehaviorError> {`);
|
|
503
|
-
lines.push(` ${innerDecode}(&wire)`);
|
|
504
|
-
break;
|
|
505
|
-
case "arr":
|
|
506
|
-
lines.push(`fn ${fn}<W: WireRow>(wire: Vec<W>) -> Result<${resultType}, BehaviorError> {`);
|
|
507
|
-
lines.push(` let mut out = Vec::with_capacity(wire.len());`);
|
|
508
|
-
lines.push(` for w in wire {`);
|
|
509
|
-
lines.push(` out.push(${innerDecode}(&w)?);`);
|
|
510
|
-
lines.push(` }`);
|
|
511
|
-
lines.push(` Ok(out)`);
|
|
512
|
-
break;
|
|
513
|
-
case "opt":
|
|
514
|
-
lines.push(`fn ${fn}<W: WireRow>(wire: Option<W>) -> Result<${resultType}, BehaviorError> {`);
|
|
515
|
-
lines.push(` match wire {`);
|
|
516
|
-
lines.push(` Some(w) => Ok(Some(${innerDecode}(&w)?)),`);
|
|
517
|
-
lines.push(` None => Ok(None),`);
|
|
518
|
-
lines.push(` }`);
|
|
519
|
-
break;
|
|
520
|
-
case "map":
|
|
521
|
-
lines.push(`fn ${fn}<W: WireRow>(wire: std::collections::BTreeMap<String, W>) -> Result<${resultType}, BehaviorError> {`);
|
|
522
|
-
lines.push(` let mut out = std::collections::BTreeMap::new();`);
|
|
523
|
-
lines.push(` for (k, w) in wire {`);
|
|
524
|
-
lines.push(` out.insert(k, ${innerDecode}(&w)?);`);
|
|
525
|
-
lines.push(` }`);
|
|
526
|
-
lines.push(` Ok(out)`);
|
|
527
|
-
break;
|
|
528
|
-
}
|
|
529
|
-
lines.push(`}`);
|
|
530
|
-
out.push(lines.join("\n"));
|
|
531
|
-
return fn;
|
|
532
|
-
}
|
|
533
|
-
function emitRustNodeDecodeFns(comp, typedNodes, plan, emitted, out) {
|
|
534
|
-
for (const n of comp.body) {
|
|
535
|
-
if ("cond" in n)
|
|
536
|
-
continue;
|
|
537
|
-
if ("map" in n || "fanout" in n) {
|
|
538
|
-
const node = n;
|
|
539
|
-
if (!mapFanoutElemDeBoxEligible(node, typedNodes, plan))
|
|
540
|
-
continue;
|
|
541
|
-
emitRustDecodeElemRow(comp, node, mapFanoutElemRef(node, typedNodes, plan), plan, emitted, out);
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
544
|
-
const ref = typedNodes.get(n.id);
|
|
545
|
-
if (ref === undefined)
|
|
546
|
-
continue;
|
|
547
|
-
const top = recordTop(ref);
|
|
548
|
-
if (top === null)
|
|
549
|
-
continue;
|
|
550
|
-
// a NAMED top decodes in place via decode_wire_<name>; an arr/opt/map top assembles via
|
|
551
|
-
// decode_wire_result_* (which recurses into decode_wire_<name> per record).
|
|
552
|
-
if (top.kind === "named")
|
|
553
|
-
emitRustDecodeNamed(top.innerName, plan, emitted, out);
|
|
554
|
-
else
|
|
555
|
-
emitRustDecodeWireResult(comp, n, ref, plan, emitted, out);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
/** containsNamed — whether a TypeRef transitively contains a named record (through arr / opt / map). */
|
|
559
|
-
function containsNamed(ref) {
|
|
560
|
-
switch (ref.kind) {
|
|
561
|
-
case "scalar":
|
|
562
|
-
return false;
|
|
563
|
-
case "named":
|
|
564
|
-
return true;
|
|
565
|
-
case "opt":
|
|
566
|
-
return containsNamed(ref.inner);
|
|
567
|
-
case "arr":
|
|
568
|
-
return containsNamed(ref.elem);
|
|
569
|
-
case "map":
|
|
570
|
-
return containsNamed(ref.value);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* assertDeBoxableResults — fail closed (never a silent trust) for a covered node whose de-boxable result
|
|
575
|
-
* is a NON-named top (arr / opt / map) that transitively contains a record: the generated de-box decodes
|
|
576
|
-
* records only under a NAMED top (decode_wire_* / decode_wire_elem_*), so a record inside a non-named top
|
|
577
|
-
* would be materialized on the trusting `.val` path and silently default its wire fields. That is the
|
|
578
|
-
* silent-default red line forbids, so it is a loud generation-time error (native-codegen-standard
|
|
579
|
-
* §2). A NAMED top (de-boxed) or a pure-scalar non-named top (no records to de-box) is fine.
|
|
580
|
-
*/
|
|
581
|
-
/** readsAPriorNode — a componentRef node is a DATAFLOW consumer iff at least one input port is a ref
|
|
582
|
-
* headed by a PRIOR BODY NODE (it reads a prior node's already-native result, #129), rather than a fresh
|
|
583
|
-
* external wire read whose ports come from input params / static / an element ($as) binding. Same
|
|
584
|
-
* head-is-a-body-node signal priorNodeArrElemInfo (#129) uses to lower a prior-node array feed. */
|
|
585
|
-
function readsAPriorNode(node, typedNodes) {
|
|
586
|
-
for (const expr of Object.values(node.ports ?? {})) {
|
|
587
|
-
const e = classifyExpr(expr);
|
|
588
|
-
if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0]))
|
|
589
|
-
return true;
|
|
590
|
-
}
|
|
591
|
-
return false;
|
|
592
|
-
}
|
|
593
|
-
function assertDeBoxableResults(comp, typedNodes, plan) {
|
|
594
|
-
for (const n of comp.body) {
|
|
595
|
-
if ("cond" in n)
|
|
596
|
-
continue;
|
|
597
|
-
const isMapFanout = "map" in n || "fanout" in n;
|
|
598
|
-
const ref = isMapFanout ? mapFanoutElemRef(n, typedNodes, plan) : typedNodes.get(n.id);
|
|
599
|
-
const deBoxed = isMapFanout ? ref.kind === "named" : recordTop(ref) !== null;
|
|
600
|
-
if (deBoxed)
|
|
601
|
-
continue;
|
|
602
|
-
const nodeId = n.id;
|
|
603
|
-
if (containsNamed(ref)) {
|
|
604
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: node '${nodeId}' shape ${JSON.stringify(ref)} contains a record the de-box does not decode (a deeper nesting than a direct named / arr / opt / map of a named record). It would trust the record's wire fields (silent default) — the silent-default red line forbids. Fail closed: reshape so the record is a direct de-box top, or extend the recursion for this nesting.`);
|
|
605
|
-
}
|
|
606
|
-
// a map/fanout element is ALWAYS a per-element wire read; only a named record is de-boxed
|
|
607
|
-
// (decode_wire_elem_*). A scalar / scalar-collection element materializes from the trusting per-element
|
|
608
|
-
// path — a wrong wire value silent-defaults. Fail closed.
|
|
609
|
-
if (isMapFanout) {
|
|
610
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: map/fanout node '${nodeId}' element ${JSON.stringify(ref)} is a scalar / scalar-collection per-element wire read that materializes on the trusting typed path — a wrong wire value would silent-default (diverging from run_behavior's outType check). Fail closed: declare the element as a record (de-boxed).`);
|
|
611
|
-
}
|
|
612
|
-
// a pure-scalar / scalar-collection top on a HANDLER-BACKED componentRef wire read trusts the typed
|
|
613
|
-
// materialization (a wrong wire value silent-defaults). A DATAFLOW consumer (a port fed by a prior
|
|
614
|
-
// node) computes from already-native data and generates.
|
|
615
|
-
if (!readsAPriorNode(n, typedNodes)) {
|
|
616
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: node '${nodeId}' result top ${JSON.stringify(ref)} is a handler-backed scalar / scalar-collection read that materializes on the trusting typed path — a wrong wire value would silent-default (diverging from run_behavior's outType check). Fail closed: declare the result as a record (de-boxed) or feed it as dataflow from a prior node.`);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
386
|
function staticArrElems(node) {
|
|
621
387
|
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
622
388
|
return null;
|
|
@@ -971,72 +737,6 @@ fn bc_expr_str_gt(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) == std::cmp::
|
|
|
971
737
|
#[allow(dead_code)]
|
|
972
738
|
fn bc_expr_str_ge(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) != std::cmp::Ordering::Less }`;
|
|
973
739
|
}
|
|
974
|
-
// ── CONCRETE per-node row structs + row→outType copiers (mirror the Go concrete contract) ──────────
|
|
975
|
-
//
|
|
976
|
-
// Each covered node has a CONCRETE row struct `RawRowNR<Comp><Node>` whose fields are the node's projected
|
|
977
|
-
// outType fields (typed, flattened). A row models a SUCCESS: failure travels the Result's Err arm as a
|
|
978
|
-
// concrete BehaviorError carrying the leaf's structured Error Value. A named-struct outType
|
|
979
|
-
// flattens to one Rust field per outType field; a scalar/arr/opt node carries a single `val: <typed>`
|
|
980
|
-
// payload. The per-component `HandlerNR<Comp>` trait has one method per node returning that concrete
|
|
981
|
-
// struct; the runner reads `row.<field>` DIRECTLY (no `RawValue`, no `match`, no `Value::`) and copies
|
|
982
|
-
// each field into the node's outType struct local.
|
|
983
|
-
/** emitRawRowStructs — emit the concrete `RawRowNR<Comp><Node>` struct per covered node (+ per-element
|
|
984
|
-
* `RawElemNR<Comp><Node>` for a covered map). A row models a SUCCESS; failure is the Result's Err arm. */
|
|
985
|
-
function emitRawRowStructs(comp, typedNodes, plan) {
|
|
986
|
-
const parts = [];
|
|
987
|
-
for (const n of comp.body) {
|
|
988
|
-
if ("cond" in n)
|
|
989
|
-
continue; // #108: a cond node has no handler-result row (pure Expression).
|
|
990
|
-
const ref = typedNodes.get(n.id);
|
|
991
|
-
if ("fanout" in n) {
|
|
992
|
-
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
993
|
-
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
994
|
-
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
|
|
995
|
-
`#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
|
|
996
|
-
continue;
|
|
997
|
-
}
|
|
998
|
-
if ("map" in n) {
|
|
999
|
-
const arrRef = ref;
|
|
1000
|
-
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
1001
|
-
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
1002
|
-
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
|
|
1003
|
-
`#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
|
|
1004
|
-
continue;
|
|
1005
|
-
}
|
|
1006
|
-
parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
|
|
1007
|
-
}
|
|
1008
|
-
return parts.join("\n\n");
|
|
1009
|
-
}
|
|
1010
|
-
/** Emit one concrete row struct: flatten a named outType to typed fields, else a single `val` payload.
|
|
1011
|
-
* A row models a SUCCESS — failure is the Result's Err arm, so no in-band error signal. */
|
|
1012
|
-
function emitOneRowStruct(name, ref, plan) {
|
|
1013
|
-
const lines = [];
|
|
1014
|
-
lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType).`);
|
|
1015
|
-
lines.push(`#[derive(Default)]`);
|
|
1016
|
-
lines.push(`pub struct ${name} {`);
|
|
1017
|
-
if (ref.kind === "named") {
|
|
1018
|
-
const decl = findDecl(plan, ref.name);
|
|
1019
|
-
for (const f of decl.fields) {
|
|
1020
|
-
lines.push(` pub ${rustFieldName(f.name)}: ${renderTypeRef(f.type)}, // ${JSON.stringify(f.name)}`);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
else {
|
|
1024
|
-
lines.push(` pub val: ${renderTypeRef(ref)},`);
|
|
1025
|
-
}
|
|
1026
|
-
lines.push(`}`);
|
|
1027
|
-
return lines.join("\n");
|
|
1028
|
-
}
|
|
1029
|
-
/** emitRowCopy — the statement copying a concrete row's typed fields into the node's outType local
|
|
1030
|
-
* `target` (a value of Rust type renderTypeRef(ref)). Named: struct literal from the row's moved fields;
|
|
1031
|
-
* scalar/arr/opt: `.val`. `rowExpr` is the concrete row VALUE (moved). No dynamic read — pure move/copy. */
|
|
1032
|
-
function emitRowMove(target, rowExpr, ref, plan) {
|
|
1033
|
-
if (ref.kind === "named") {
|
|
1034
|
-
const decl = findDecl(plan, ref.name);
|
|
1035
|
-
const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: ${rowExpr}.${rustFieldName(f.name)}`).join(", ");
|
|
1036
|
-
return `${target} = ${ref.name} { ${inits} };`;
|
|
1037
|
-
}
|
|
1038
|
-
return `${target} = ${rowExpr}.val;`;
|
|
1039
|
-
}
|
|
1040
740
|
/**
|
|
1041
741
|
* boundRustType (change #3 — typed `bound`) — the CONCRETE Rust type of a covered node's `bound` handler
|
|
1042
742
|
* param (bc#90 / runtime-free). `bound` is the parent-produced key (the bindField value); it replaces the
|
|
@@ -1084,23 +784,19 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
1084
784
|
const fnKwFor = (nodeId) => (ap.nodeIsAsync(nodeId) ? "async fn" : "fn");
|
|
1085
785
|
const lines = [];
|
|
1086
786
|
lines.push(`// ${handlerTraitName(comp.name)} — the CONCRETE per-component handler seam: one typed method`);
|
|
1087
|
-
lines.push(`// per covered node (native ports struct IN,
|
|
1088
|
-
lines.push(`// /
|
|
1089
|
-
lines.push(`//
|
|
787
|
+
lines.push(`// per covered node (native ports struct IN, WIRE value OUT). No generic boxed-ports / boxed-value`);
|
|
788
|
+
lines.push(`// / dynamic accessor crosses the covered boundary — the consumer implements each node_* by`);
|
|
789
|
+
lines.push(`// returning its own wire payload as a WireValue; the generated inline de-box turns that into the`);
|
|
790
|
+
lines.push(`// node's concrete native result (see INTEGRATION.md §6).`);
|
|
1090
791
|
lines.push(`pub trait ${handlerTraitName(comp.name)} {`);
|
|
1091
|
-
//
|
|
1092
|
-
//
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
return mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
1100
|
-
return deBoxEligible(typedNodes.get(n.id));
|
|
1101
|
-
});
|
|
1102
|
-
if (hasDeBox)
|
|
1103
|
-
lines.push(` type Wire: WireRow + Send;`);
|
|
792
|
+
// every covered node returns the consumer's WIRE (a single associated `Wire: WireValue`) — the producer's
|
|
793
|
+
// datum at the node's result-consumption point; the generated inline de-box classifies it against the
|
|
794
|
+
// STATICALLY declared type. A batched map returns one wire per element (Vec); a fanout returns the aligned
|
|
795
|
+
// per-id wires (Vec<Option<Wire>> — None = a DANGLING id). The `+ Send` bound lets the wire cross the
|
|
796
|
+
// parallel-stage runner's scoped fan-out threads.
|
|
797
|
+
const hasHandler = comp.body.some((n) => !("cond" in n));
|
|
798
|
+
if (hasHandler)
|
|
799
|
+
lines.push(` type Wire: WireValue + Send;`);
|
|
1104
800
|
for (const n of comp.body) {
|
|
1105
801
|
if ("cond" in n)
|
|
1106
802
|
continue; // #108: a cond node has no handler method (pure Expression).
|
|
@@ -1108,29 +804,23 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
1108
804
|
const fnKw = fnKwFor(n.id);
|
|
1109
805
|
const bt = boundRustType(n, typedNodes, plan);
|
|
1110
806
|
if ("fanout" in n) {
|
|
1111
|
-
// fanout (v3): one batched dispatch of the deduped id list
|
|
1112
|
-
//
|
|
1113
|
-
//
|
|
807
|
+
// fanout (v3): one batched dispatch of the deduped id list, returning the aligned element WIRES the
|
|
808
|
+
// runner de-boxes then dedupe/drops. Each aligned position is Option<Self::Wire> — None = a DANGLING
|
|
809
|
+
// id (the runner maps it to the zero elem row).
|
|
1114
810
|
const portsT = `${portsStructName(comp.name, n.id)}Batch`;
|
|
1115
|
-
|
|
1116
|
-
const retT = elemDeBox ? "Vec<Option<Self::Wire>>" : rawRowStructName(comp.name, n.id);
|
|
1117
|
-
lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Result<${retT}, BehaviorError>;`);
|
|
811
|
+
lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Result<Vec<Option<Self::Wire>>, BehaviorError>;`);
|
|
1118
812
|
continue;
|
|
1119
813
|
}
|
|
1120
814
|
if ("map" in n) {
|
|
1121
815
|
const m = n.map;
|
|
1122
816
|
const batched = m.batched === true;
|
|
1123
817
|
const portsT = batched ? `${portsStructName(comp.name, n.id)}Batch` : portsStructName(comp.name, n.id);
|
|
1124
|
-
// a
|
|
1125
|
-
const
|
|
1126
|
-
const retT = elemDeBox ? (batched ? "Vec<Self::Wire>" : "Self::Wire") : batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
|
|
818
|
+
// a map returns the element WIRE(s): batched → Vec<Self::Wire>, non-batched → Self::Wire.
|
|
819
|
+
const retT = batched ? "Vec<Self::Wire>" : "Self::Wire";
|
|
1127
820
|
lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Result<${retT}, BehaviorError>;`);
|
|
1128
821
|
continue;
|
|
1129
822
|
}
|
|
1130
|
-
|
|
1131
|
-
const top = ref !== undefined ? recordTop(ref) : null;
|
|
1132
|
-
const retT = top !== null ? rustWireTypeForTop(top) : rawRowStructName(comp.name, n.id);
|
|
1133
|
-
lines.push(` ${fnKw} ${method}(&self, ports: &${portsStructName(comp.name, n.id)}, bound: ${bt}) -> Result<${retT}, BehaviorError>;`);
|
|
823
|
+
lines.push(` ${fnKw} ${method}(&self, ports: &${portsStructName(comp.name, n.id)}, bound: ${bt}) -> Result<Self::Wire, BehaviorError>;`);
|
|
1134
824
|
}
|
|
1135
825
|
lines.push(`}`);
|
|
1136
826
|
return lines.join("\n");
|
|
@@ -1172,9 +862,8 @@ function mapElemRowRef(node, arrRef, plan) {
|
|
|
1172
862
|
}
|
|
1173
863
|
/**
|
|
1174
864
|
* emitMapElemMaterializers — for each covered map...into node, emit the AUGMENTED-element copier: copy
|
|
1175
|
-
* the over element's typed fields +
|
|
1176
|
-
*
|
|
1177
|
-
* typed into row).
|
|
865
|
+
* the over element's typed fields + place the already-de-boxed `into` value into its field. Pure struct
|
|
866
|
+
* field assignment — NO dynamic value read (the inline de-box already produced the typed `into` value).
|
|
1178
867
|
*/
|
|
1179
868
|
function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
1180
869
|
const parts = [];
|
|
@@ -1196,7 +885,6 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
|
1196
885
|
const overFields = decl.fields.filter((f) => f.name !== intoKey);
|
|
1197
886
|
const overElemTy = mapOverElemType(comp, n, typedNodes, plan);
|
|
1198
887
|
const fn = mapElemMaterializerName(comp.name, n.id);
|
|
1199
|
-
const elemRowGo = rawElemStructName(comp.name, n.id);
|
|
1200
888
|
const intoRowRef = mapElemRowRef(n, arrRef, plan);
|
|
1201
889
|
const inits = [];
|
|
1202
890
|
// Copy scalars (i64/f64/bool) copy directly; String/named/arr/opt clone (clippy clone_on_copy clean).
|
|
@@ -1204,15 +892,12 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
|
1204
892
|
const isCopy = f.type.kind === "scalar" && (f.type.scalar === "int" || f.type.scalar === "float" || f.type.scalar === "bool");
|
|
1205
893
|
inits.push(` ${rustFieldName(f.name)}: over.${rustFieldName(f.name)}${isCopy ? "" : ".clone()"},`);
|
|
1206
894
|
}
|
|
1207
|
-
//
|
|
1208
|
-
const intoValue = intoRowRef.kind === "named"
|
|
1209
|
-
? `${intoRowRef.name} { ${findDecl(plan, intoRowRef.name).fields.map((f) => `${rustFieldName(f.name)}: into.${rustFieldName(f.name)}`).join(", ")} }`
|
|
1210
|
-
: `into.val`;
|
|
895
|
+
// the `into` param is the already-de-boxed into value (its native type) — drop it straight in.
|
|
1211
896
|
parts.push(`#[allow(dead_code)]
|
|
1212
|
-
fn ${fn}(over: &${overElemTy}, into: ${
|
|
897
|
+
fn ${fn}(over: &${overElemTy}, into: ${renderTypeRef(intoRowRef)}) -> ${elemRef.name} {
|
|
1213
898
|
${elemRef.name} {
|
|
1214
899
|
${inits.join("\n")}
|
|
1215
|
-
${rustFieldName(intoKey)}:
|
|
900
|
+
${rustFieldName(intoKey)}: into,
|
|
1216
901
|
}
|
|
1217
902
|
}`);
|
|
1218
903
|
void intoField;
|
|
@@ -1739,11 +1424,11 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1739
1424
|
const lines = [];
|
|
1740
1425
|
lines.push(`// run_native_raw_struct_${fn} — the STRUCT-RETURNING combined read (bc#77/#87/#94): the fully`);
|
|
1741
1426
|
lines.push(`// de-plumbed CONCRETE path. Generic over the per-component CONCRETE ${handlerTraitName(comp.name)} trait,`);
|
|
1742
|
-
lines.push(`// whose node_* methods take the node's native ports struct and return the
|
|
1743
|
-
lines.push(`//
|
|
1744
|
-
lines.push(`//
|
|
1745
|
-
lines.push(`//
|
|
1746
|
-
lines.push(`// handler result, no generic enum crossing, no dispatch on a dynamic value on the covered plane. Node`);
|
|
1427
|
+
lines.push(`// whose node_* methods take the node's native ports struct and return the node's result WIRE`);
|
|
1428
|
+
lines.push(`// (Self::Wire: WireValue). The runner builds each ports struct by direct native construction (a`);
|
|
1429
|
+
lines.push(`// simple string port lowers to a native Rust expr), dispatches the concrete node_* method, and`);
|
|
1430
|
+
lines.push(`// de-boxes the result wire INLINE (match wire.as_*() { … } fully unrolled — no decode helper) into`);
|
|
1431
|
+
lines.push(`// the node's outType struct cell — no boxed handler result, no generic enum crossing, no dispatch on a dynamic value on the covered plane. Node`);
|
|
1747
1432
|
lines.push(`// results are typed struct cells; a relation child reads the parent's REAL struct result via`);
|
|
1748
1433
|
lines.push(`// direct field access (child-present decision from the real parent value — relationSingle /`);
|
|
1749
1434
|
lines.push(`// connection converge). A real-concurrency stage (bc#87) is static parallel orchestration —`);
|
|
@@ -1847,57 +1532,33 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1847
1532
|
// directly (no `.await`). The runner being `async fn` does not force every call site to await.
|
|
1848
1533
|
const awaitSfx = ap.nodeIsAsync(node.id) ? ".await" : "";
|
|
1849
1534
|
lines.push(`${indent}let ports_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
1850
|
-
//
|
|
1851
|
-
//
|
|
1852
|
-
// (byte-equal codes to run_behavior's outType check, so ≡ holds).
|
|
1853
|
-
// concrete-row move.
|
|
1854
|
-
const top = recordTop(ref);
|
|
1855
|
-
const namedTop = top !== null && top.kind === "named";
|
|
1856
|
-
const deBox = top !== null;
|
|
1535
|
+
// uniform seam: every covered node returns a single result WIRE (Self::Wire); the inline de-box
|
|
1536
|
+
// (renderResultDeBox) classifies it against the STATICALLY declared result type and produces the
|
|
1537
|
+
// structured Error Value on a mismatch (byte-equal codes to run_behavior's outType check, so ≡ holds).
|
|
1857
1538
|
if (meta.policy === "continue") {
|
|
1858
|
-
// continue: a failed node (leaf failure OR de-box mismatch) produces no Port — the downstream
|
|
1859
|
-
//
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
lines.push(`${indent} }`);
|
|
1874
|
-
lines.push(`${indent}}`);
|
|
1875
|
-
}
|
|
1876
|
-
else {
|
|
1877
|
-
lines.push(`${indent}if let Ok(row_${s}) = handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
|
|
1878
|
-
lines.push(`${indent} ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
1879
|
-
lines.push(`${indent} produced_${s}.set(true);`);
|
|
1880
|
-
lines.push(`${indent}}`);
|
|
1881
|
-
}
|
|
1539
|
+
// continue: a failed node (leaf failure OR de-box mismatch) produces no Port — the downstream Skip
|
|
1540
|
+
// propagation is what `continue` means. The inline de-box's mismatch breaks the labeled block (a
|
|
1541
|
+
// local skip, NOT a runner-wide propagate); only a clean decode commits. (Covered componentRef nodes
|
|
1542
|
+
// are always 'fail' policy today — this arm is defensive.)
|
|
1543
|
+
const label = `debox_${s}`;
|
|
1544
|
+
const contFail = (e) => `break '${label} Err(${e})`;
|
|
1545
|
+
lines.push(`${indent}if let Ok(wire_${s}) = handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
|
|
1546
|
+
lines.push(`${indent} let decoded_${s}: Result<${renderTypeRef(ref)}, BehaviorError> = '${label}: {`);
|
|
1547
|
+
lines.push(`${indent} Ok(${renderResultDeBox(ref, `wire_${s}`, node.id, plan, `${indent} `, contFail)})`);
|
|
1548
|
+
lines.push(`${indent} };`);
|
|
1549
|
+
lines.push(`${indent} if let Ok(val_${s}) = decoded_${s} {`);
|
|
1550
|
+
lines.push(`${indent} *${typedCell(node.id)}.borrow_mut() = val_${s};`);
|
|
1551
|
+
lines.push(`${indent} produced_${s}.set(true);`);
|
|
1552
|
+
lines.push(`${indent} }`);
|
|
1553
|
+
lines.push(`${indent}}`);
|
|
1882
1554
|
}
|
|
1883
1555
|
else {
|
|
1884
1556
|
const policyLit = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
|
|
1885
|
-
|
|
1886
|
-
lines.push(`${indent}let ${resultVar} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
|
|
1557
|
+
lines.push(`${indent}let wire_${s} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
|
|
1887
1558
|
lines.push(`${indent} Ok(r) => r,`);
|
|
1888
1559
|
lines.push(`${indent} Err(e) => return Err(BehaviorError { code: "OP_FAILED".to_string(), message: format!("operation '{}' failed under '${policyLit}: {}", ${rustStrLit(node.id)}, e.message), detail: e.detail }),`);
|
|
1889
1560
|
lines.push(`${indent}};`);
|
|
1890
|
-
|
|
1891
|
-
lines.push(`${indent}let row_${s} = ${rustDecodeNamedName(top.innerName)}(&wire_${s})?;`);
|
|
1892
|
-
lines.push(`${indent}${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
1893
|
-
}
|
|
1894
|
-
else if (deBox) {
|
|
1895
|
-
lines.push(`${indent}let res_${s} = ${rustDecodeResultName(comp.name, node.id)}(wire_${s})?;`);
|
|
1896
|
-
lines.push(`${indent}*${typedCell(node.id)}.borrow_mut() = res_${s};`);
|
|
1897
|
-
}
|
|
1898
|
-
else {
|
|
1899
|
-
lines.push(`${indent}${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
1900
|
-
}
|
|
1561
|
+
lines.push(`${indent}*${typedCell(node.id)}.borrow_mut() = ${renderResultDeBox(ref, `wire_${s}`, node.id, plan, indent)};`);
|
|
1901
1562
|
lines.push(`${indent}produced_${s}.set(true);`);
|
|
1902
1563
|
}
|
|
1903
1564
|
for (const c of closers)
|
|
@@ -2023,25 +1684,11 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
2023
1684
|
const boundN = Math.min(concurrency, nMembers);
|
|
2024
1685
|
const jobsVar = `jobs_${sanitize(comp.name)}`;
|
|
2025
1686
|
const hWire = `<H as ${handlerTraitName(comp.name)}>::Wire`;
|
|
2026
|
-
const hWireTop = (top) => {
|
|
2027
|
-
switch (top.kind) {
|
|
2028
|
-
case "named":
|
|
2029
|
-
return hWire;
|
|
2030
|
-
case "arr":
|
|
2031
|
-
return `Vec<${hWire}>`;
|
|
2032
|
-
case "opt":
|
|
2033
|
-
return `Option<${hWire}>`;
|
|
2034
|
-
case "map":
|
|
2035
|
-
return `std::collections::BTreeMap<String, ${hWire}>`;
|
|
2036
|
-
}
|
|
2037
|
-
};
|
|
2038
1687
|
for (const i of refMembers) {
|
|
2039
1688
|
const s = sanitize(comp.body[i].id);
|
|
2040
|
-
//
|
|
2041
|
-
//
|
|
2042
|
-
|
|
2043
|
-
const slotT = memTop !== null ? hWireTop(memTop) : rawRowStructName(comp.name, comp.body[i].id);
|
|
2044
|
-
lines.push(` let slot_${s}: std::sync::Mutex<Option<Result<${slotT}, BehaviorError>>> = std::sync::Mutex::new(None);`);
|
|
1689
|
+
// every member's thread returns the result WIRE (a single Self::Wire); the ascending COMMIT phase
|
|
1690
|
+
// de-boxes it (so the wire — not the decode — is what crosses the scoped threads).
|
|
1691
|
+
lines.push(` let slot_${s}: std::sync::Mutex<Option<Result<${hWire}, BehaviorError>>> = std::sync::Mutex::new(None);`);
|
|
2045
1692
|
}
|
|
2046
1693
|
lines.push(` let mut ${jobsVar}: Vec<usize> = Vec::new();`);
|
|
2047
1694
|
refMembers.forEach((i, slot) => {
|
|
@@ -2080,51 +1727,28 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
2080
1727
|
const ref = typedNodes.get(node.id);
|
|
2081
1728
|
lines.push(` if ports_${s}.is_some() {`);
|
|
2082
1729
|
lines.push(` let out_${s} = slot_${s}.lock().unwrap().take().expect("runnable member wrote its slot");`);
|
|
2083
|
-
const top = recordTop(ref);
|
|
2084
|
-
const namedTop = top !== null && top.kind === "named";
|
|
2085
|
-
const deBox = top !== null;
|
|
2086
1730
|
if (meta.policy === "continue") {
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
lines.push(` }`);
|
|
2101
|
-
lines.push(` }`);
|
|
2102
|
-
}
|
|
2103
|
-
else {
|
|
2104
|
-
lines.push(` if let Ok(row_${s}) = out_${s} {`);
|
|
2105
|
-
lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
2106
|
-
lines.push(` produced_${s}.set(true);`);
|
|
2107
|
-
lines.push(` }`);
|
|
2108
|
-
}
|
|
1731
|
+
// continue: a failed member (leaf failure OR de-box mismatch) commits no Port (defensive — covered
|
|
1732
|
+
// componentRef members are always 'fail' policy today).
|
|
1733
|
+
const label = `debox_${s}`;
|
|
1734
|
+
const contFail = (e) => `break '${label} Err(${e})`;
|
|
1735
|
+
lines.push(` if let Ok(wire_${s}) = out_${s} {`);
|
|
1736
|
+
lines.push(` let decoded_${s}: Result<${renderTypeRef(ref)}, BehaviorError> = '${label}: {`);
|
|
1737
|
+
lines.push(` Ok(${renderResultDeBox(ref, `wire_${s}`, node.id, plan, " ", contFail)})`);
|
|
1738
|
+
lines.push(` };`);
|
|
1739
|
+
lines.push(` if let Ok(val_${s}) = decoded_${s} {`);
|
|
1740
|
+
lines.push(` *${typedCell(node.id)}.borrow_mut() = val_${s};`);
|
|
1741
|
+
lines.push(` produced_${s}.set(true);`);
|
|
1742
|
+
lines.push(` }`);
|
|
1743
|
+
lines.push(` }`);
|
|
2109
1744
|
}
|
|
2110
1745
|
else {
|
|
2111
1746
|
const policyLit = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
|
|
2112
|
-
|
|
2113
|
-
lines.push(` let ${resultVar} = match out_${s} {`);
|
|
1747
|
+
lines.push(` let wire_${s} = match out_${s} {`);
|
|
2114
1748
|
lines.push(` Ok(r) => r,`);
|
|
2115
1749
|
lines.push(` Err(e) => return Err(BehaviorError { code: "OP_FAILED".to_string(), message: format!("operation '{}' failed under '${policyLit}: {}", ${rustStrLit(node.id)}, e.message), detail: e.detail }),`);
|
|
2116
1750
|
lines.push(` };`);
|
|
2117
|
-
|
|
2118
|
-
lines.push(` let row_${s} = ${rustDecodeNamedName(top.innerName)}(&wire_${s})?;`);
|
|
2119
|
-
lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
2120
|
-
}
|
|
2121
|
-
else if (deBox) {
|
|
2122
|
-
lines.push(` let res_${s} = ${rustDecodeResultName(comp.name, node.id)}(wire_${s})?;`);
|
|
2123
|
-
lines.push(` *${typedCell(node.id)}.borrow_mut() = res_${s};`);
|
|
2124
|
-
}
|
|
2125
|
-
else {
|
|
2126
|
-
lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
2127
|
-
}
|
|
1751
|
+
lines.push(` *${typedCell(node.id)}.borrow_mut() = ${renderResultDeBox(ref, `wire_${s}`, node.id, plan, " ")};`);
|
|
2128
1752
|
lines.push(` produced_${s}.set(true);`);
|
|
2129
1753
|
}
|
|
2130
1754
|
lines.push(` }`);
|
|
@@ -2161,11 +1785,11 @@ isAsync = false) {
|
|
|
2161
1785
|
const portNames = Object.keys(m.ports);
|
|
2162
1786
|
const elemArrRef = typedNodes.get(node.id);
|
|
2163
1787
|
const elemName = renderTypeRef(elemArrRef.elem);
|
|
2164
|
-
//
|
|
2165
|
-
//
|
|
2166
|
-
// elementPolicy (matching the interpreter's whole-node outType throw).
|
|
2167
|
-
const
|
|
2168
|
-
const
|
|
1788
|
+
// each element is de-boxed strictly INLINE: the handler returns the per-element WIRE and the arm decodes
|
|
1789
|
+
// it against the element ref (the `into` field's type for map...into, else the element type). A de-box
|
|
1790
|
+
// mismatch fails closed regardless of elementPolicy (matching the interpreter's whole-node outType throw).
|
|
1791
|
+
const elemDeBoxRef = mapElemRowRef(node, elemArrRef, plan);
|
|
1792
|
+
const renderElemDeBox = (il) => renderResultDeBox(elemDeBoxRef, `er_${s}`, node.id, plan, il);
|
|
2169
1793
|
// build the per-element CONCRETE native ports struct. `il` = indent inside the element loop.
|
|
2170
1794
|
const buildPorts = (il) => {
|
|
2171
1795
|
const out = [];
|
|
@@ -2225,28 +1849,23 @@ isAsync = false) {
|
|
|
2225
1849
|
lines.push(`${indent} Ok(r) => r,`);
|
|
2226
1850
|
lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
|
|
2227
1851
|
lines.push(`${indent} };`);
|
|
2228
|
-
|
|
2229
|
-
const batchIter = elemDeBox ? `row_${s}.into_iter()` : `row_${s}.rows.into_iter()`;
|
|
2230
|
-
lines.push(`${indent} if ${batchLen} != want_${s} {`);
|
|
1852
|
+
lines.push(`${indent} if row_${s}.len() != want_${s} {`);
|
|
2231
1853
|
lines.push(`${indent} return Err(BehaviorError::new("MAP_BATCH_RESULT_MISMATCH", format!("map '{}': batched handler must return a list aligned to items (want {})", ${rustStrLit(node.id)}, want_${s})));`);
|
|
2232
1854
|
lines.push(`${indent} }`);
|
|
2233
1855
|
if (hasInto) {
|
|
2234
|
-
// zip the aligned
|
|
2235
|
-
//
|
|
2236
|
-
lines.push(`${indent} let mut rows_iter_${s} = ${
|
|
1856
|
+
// zip the aligned per-element WIRES onto each KEPT over element: de-box the element wire strictly
|
|
1857
|
+
// (into the `into` field's type) then materialize the augmented element.
|
|
1858
|
+
lines.push(`${indent} let mut rows_iter_${s} = row_${s}.into_iter();`);
|
|
2237
1859
|
lines.push(`${indent} for over_el_${s} in kept_${s}.iter() {`);
|
|
2238
1860
|
lines.push(`${indent} let er_${s} = rows_iter_${s}.next().expect("aligned batch row");`);
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
lines.push(`${indent} built_${s}.push(${elemMat}(over_el_${s}, er_${s}));`);
|
|
1861
|
+
lines.push(`${indent} let dec_${s} = ${renderElemDeBox(`${indent} `)};`);
|
|
1862
|
+
lines.push(`${indent} built_${s}.push(${elemMat}(over_el_${s}, dec_${s}));`);
|
|
2242
1863
|
lines.push(`${indent} }`);
|
|
2243
1864
|
}
|
|
2244
1865
|
else {
|
|
2245
|
-
// no-into:
|
|
2246
|
-
lines.push(`${indent} for er_${s} in ${
|
|
2247
|
-
|
|
2248
|
-
lines.push(`${indent} let er_${s} = ${decElemFn}(&er_${s})?;`);
|
|
2249
|
-
lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
|
|
1866
|
+
// no-into: de-box each aligned per-element wire DIRECTLY into the element (result = kept elements).
|
|
1867
|
+
lines.push(`${indent} for er_${s} in row_${s}.into_iter() {`);
|
|
1868
|
+
lines.push(`${indent} let el_${s} = ${renderElemDeBox(`${indent} `)};`);
|
|
2250
1869
|
lines.push(`${indent} built_${s}.push(el_${s});`);
|
|
2251
1870
|
lines.push(`${indent} }`);
|
|
2252
1871
|
}
|
|
@@ -2271,16 +1890,14 @@ isAsync = false) {
|
|
|
2271
1890
|
lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
|
|
2272
1891
|
}
|
|
2273
1892
|
lines.push(`${indent} };`);
|
|
2274
|
-
//
|
|
2275
|
-
|
|
2276
|
-
lines.push(`${indent} let er_${s} = ${decElemFn}(&er_${s})?;`);
|
|
1893
|
+
// the element's wire is de-boxed strictly INLINE (fail-closed regardless of elementPolicy).
|
|
1894
|
+
lines.push(`${indent} let dec_${s} = ${renderElemDeBox(`${indent} `)};`);
|
|
2277
1895
|
if (hasInto) {
|
|
2278
|
-
lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s},
|
|
1896
|
+
lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s}, dec_${s}));`);
|
|
2279
1897
|
}
|
|
2280
1898
|
else {
|
|
2281
1899
|
lines.push(`${indent} let _ = &oel_${s};`);
|
|
2282
|
-
lines.push(`${indent} ${
|
|
2283
|
-
lines.push(`${indent} built_${s}.push(el_${s});`);
|
|
1900
|
+
lines.push(`${indent} built_${s}.push(dec_${s});`);
|
|
2284
1901
|
}
|
|
2285
1902
|
lines.push(`${indent}}`);
|
|
2286
1903
|
}
|
|
@@ -2349,11 +1966,9 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
|
|
|
2349
1966
|
lines.push(`${indent} Ok(r) => r,`);
|
|
2350
1967
|
lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
|
|
2351
1968
|
lines.push(`${indent} };`);
|
|
2352
|
-
// the
|
|
2353
|
-
//
|
|
2354
|
-
//
|
|
2355
|
-
const decElemFn = rustDecodeElemName(comp.name, node.id);
|
|
2356
|
-
const rawElemT = rawElemStructName(comp.name, node.id);
|
|
1969
|
+
// the handler returns the aligned per-id element WIRES (Vec<Option<Self::Wire>>); each present body is
|
|
1970
|
+
// de-boxed strictly INLINE (a de-box mismatch fails closed). A DANGLING id is None → the zero element
|
|
1971
|
+
// (empty dedupeKey), which the dangling drop excludes.
|
|
2357
1972
|
lines.push(`${indent} if row_${s}.len() != want_${s} {`);
|
|
2358
1973
|
lines.push(`${indent} return Err(BehaviorError::new("FANOUT_BATCH_RESULT_MISMATCH", format!("fanout '{}': batched handler must return a list aligned to the deduped id list (want {})", ${rustStrLit(node.id)}, want_${s})));`);
|
|
2359
1974
|
lines.push(`${indent} }`);
|
|
@@ -2362,11 +1977,10 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
|
|
|
2362
1977
|
// structural (the elem type already omits it). cursor is always None (connection wrap).
|
|
2363
1978
|
lines.push(`${indent} let mut seen_${s}: std::collections::HashSet<String> = std::collections::HashSet::new();`);
|
|
2364
1979
|
lines.push(`${indent} for opt_er_${s} in row_${s}.into_iter() {`);
|
|
2365
|
-
lines.push(`${indent} let
|
|
2366
|
-
lines.push(`${indent} Some(wire_${s}) => ${
|
|
1980
|
+
lines.push(`${indent} let el_${s}: ${elemName} = match opt_er_${s} {`);
|
|
1981
|
+
lines.push(`${indent} Some(wire_${s}) => ${renderResultDeBox(elemRef, `wire_${s}`, node.id, plan, `${indent} `)},`);
|
|
2367
1982
|
lines.push(`${indent} None => Default::default(),`);
|
|
2368
1983
|
lines.push(`${indent} };`);
|
|
2369
|
-
lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemRef, plan)}`);
|
|
2370
1984
|
lines.push(`${indent} let key_${s} = el_${s}.${dkRust}.clone();`);
|
|
2371
1985
|
if (f.drop === "dangling") {
|
|
2372
1986
|
lines.push(`${indent} if key_${s}.is_empty() { continue; }`);
|
|
@@ -2444,16 +2058,6 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
|
|
|
2444
2058
|
lines.push(cl);
|
|
2445
2059
|
return lines.join("\n");
|
|
2446
2060
|
}
|
|
2447
|
-
/** emitElemMove — move a concrete element row (RawElemNR, error already handled) into a fresh `let` local
|
|
2448
|
-
* of the element outType. Named → struct literal from moved fields; scalar/arr/opt → .val. */
|
|
2449
|
-
function emitElemMove(letBind, rowExpr, ref, plan) {
|
|
2450
|
-
if (ref.kind === "named") {
|
|
2451
|
-
const decl = findDecl(plan, ref.name);
|
|
2452
|
-
const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: ${rowExpr}.${rustFieldName(f.name)}`).join(", ");
|
|
2453
|
-
return `${letBind} = ${ref.name} { ${inits} };`;
|
|
2454
|
-
}
|
|
2455
|
-
return `${letBind} = ${rowExpr}.val;`;
|
|
2456
|
-
}
|
|
2457
2061
|
/**
|
|
2458
2062
|
* emitProducedAwareValue — the #86 part 1 produced-aware output lowering (rust twin of the go one).
|
|
2459
2063
|
*/
|
|
@@ -2575,13 +2179,12 @@ ${emitStructSurface([])}
|
|
|
2575
2179
|
// #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler).
|
|
2576
2180
|
rustExprUsed.helpers = false;
|
|
2577
2181
|
const structs = [];
|
|
2578
|
-
const rowStructs = [];
|
|
2579
2182
|
const handlerTraits = [];
|
|
2580
2183
|
const inStructs = [];
|
|
2581
2184
|
const elemMaterializers = [];
|
|
2582
|
-
// the
|
|
2583
|
-
|
|
2584
|
-
const
|
|
2185
|
+
// every covered node returns a WireValue the inline de-box classifies — the wire seam (probe enums +
|
|
2186
|
+
// WireValue/WireRow/WireList traits) is emitted once whenever any handler node exists.
|
|
2187
|
+
const anyWire = native.some((c) => c.body.some((n) => !("cond" in n)));
|
|
2585
2188
|
for (const c of native) {
|
|
2586
2189
|
const typedNodes = new Map();
|
|
2587
2190
|
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
@@ -2599,11 +2202,7 @@ ${emitStructSurface([])}
|
|
|
2599
2202
|
else
|
|
2600
2203
|
structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
|
|
2601
2204
|
}
|
|
2602
|
-
// fail closed for a record inside a non-named top (never a silent trust) — before emitting anything.
|
|
2603
|
-
assertDeBoxableResults(c, typedNodes, plan);
|
|
2604
|
-
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
2605
2205
|
handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
|
|
2606
|
-
emitRustNodeDecodeFns(c, typedNodes, plan, decodeEmitted, decodeFns);
|
|
2607
2206
|
inStructs.push(emitInStruct(c, plan));
|
|
2608
2207
|
const mm = emitMapElemMaterializers(c, typedNodes, plan);
|
|
2609
2208
|
if (mm)
|
|
@@ -2618,9 +2217,9 @@ ${emitStructSurface([])}
|
|
|
2618
2217
|
// relationKind:single children), emitted ONLY as a STRUCT-returning runner GENERIC over a per-component
|
|
2619
2218
|
// CONCRETE handler trait (HandlerNR<comp>): native ports struct IN (direct construction — every port is a
|
|
2620
2219
|
// CONCRETE Rust value: String / bool / Vec<&str> projection / i64 limit — NO boxed Value, NO by-name
|
|
2621
|
-
// accessor); the handler takes a typed produced-aware 'bound' Option and returns the node's
|
|
2622
|
-
//
|
|
2623
|
-
// outType struct. The covered plane carries NO bc-runtime reference at all — failures use the LOCAL
|
|
2220
|
+
// accessor); the handler takes a typed produced-aware 'bound' Option and returns the node's result WIRE
|
|
2221
|
+
// (Self::Wire: WireValue), and the runner de-boxes it INLINE (fully-unrolled probe/match — no decode helper,
|
|
2222
|
+
// no output recursion) into the node's outType struct. The covered plane carries NO bc-runtime reference at all — failures use the LOCAL
|
|
2624
2223
|
// BehaviorError (same codes, byte-equal to run_behavior). INLINE sequential (no plan driver) so a relation
|
|
2625
2224
|
// child reads the parent's REAL struct result via direct field access and the child-present decision is
|
|
2626
2225
|
// made from the real parent value — relationSingle / connection converge with run_behavior (fixes
|
|
@@ -2641,16 +2240,10 @@ use std::cell::RefCell;`;
|
|
|
2641
2240
|
structs.some((r) => r.trim().length > 0)
|
|
2642
2241
|
? `// Native ports structs (one per componentRef node; typed per the static port type — CONCRETE).\n${structs.filter((r) => r.trim().length > 0).join("\n\n")}`
|
|
2643
2242
|
: undefined,
|
|
2644
|
-
rowStructs.some((r) => r.trim().length > 0)
|
|
2645
|
-
? `// CONCRETE per-node row structs (typed fields = the node's outType; + error signal).\n${rowStructs.filter((r) => r.trim().length > 0).join("\n\n")}`
|
|
2646
|
-
: undefined,
|
|
2647
2243
|
`// CONCRETE per-component input structs (fields = inputPorts).\n${inStructs.join("\n\n")}`,
|
|
2648
|
-
`// CONCRETE per-component handler traits (one node_* method per node — native ports IN,
|
|
2649
|
-
|
|
2650
|
-
? `// strict de-box wire seam — the consumer implements WireRow/WireList over its wire payload (variant
|
|
2651
|
-
: undefined,
|
|
2652
|
-
decodeFns.length
|
|
2653
|
-
? `// Generated strict de-box functions (per named record type; structured Error Value on mismatch).\n${decodeFns.join("\n\n")}`
|
|
2244
|
+
`// CONCRETE per-component handler traits (one node_* method per node — native ports IN, WIRE value OUT).\n${handlerTraits.join("\n\n")}`,
|
|
2245
|
+
anyWire
|
|
2246
|
+
? `// strict de-box wire seam — the consumer implements WireValue (+ WireRow/WireList reached via\n// as_row/as_list) over its wire payload (variant classification); the generated INLINE de-box owns\n// strictness (required/optional, present/absent, error assembly, fail-closed). Concrete enums +\n// monomorphized traits — no boxed value, no trait object, no heap allocation crosses the seam.\n${emitRustProbeWireTypes()}`
|
|
2654
2247
|
: undefined,
|
|
2655
2248
|
elemMaterializers.length
|
|
2656
2249
|
? `// map augmented-element copiers (pure struct field assignment — no dynamic value read).\n${elemMaterializers.join("\n\n")}`
|
|
@@ -2683,25 +2276,14 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2683
2276
|
const anyAsyncRunner = native.some((c) => asyncPlans.get(c.name).runnerIsAsync);
|
|
2684
2277
|
// ── per-component input decoders: generic &[(String,Value)] -> concrete InNR_<comp> (test glue). ──
|
|
2685
2278
|
const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
|
|
2686
|
-
//
|
|
2687
|
-
//
|
|
2688
|
-
const anyDeBox = native.some((c) =>
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
if ("cond" in n)
|
|
2695
|
-
return false;
|
|
2696
|
-
if ("map" in n || "fanout" in n)
|
|
2697
|
-
return mapFanoutElemDeBoxEligible(n, tn, plan);
|
|
2698
|
-
return deBoxEligible(tn.get(n.id));
|
|
2699
|
-
});
|
|
2700
|
-
});
|
|
2701
|
-
// scripted wire adapter (TEST glue): wraps a scripted Value as a WireRow/WireList and classifies each
|
|
2702
|
-
// attribute's native type into a DynamoDB-flavored wire tag (S/N/BOOL/L/M/NULL). A real consumer
|
|
2703
|
-
// implements WireRow over its own AttributeValue map; this drives the generated de-box from the same
|
|
2704
|
-
// scripted vectors run_behavior uses — so actualWireType observed in a mismatch is the PRODUCER tag.
|
|
2279
|
+
// every covered node returns a WireValue the inline de-box classifies — so the scripted wire adapter is
|
|
2280
|
+
// needed whenever any component has a handler node.
|
|
2281
|
+
const anyDeBox = native.some((c) => c.body.some((n) => !("cond" in n)));
|
|
2282
|
+
// scripted wire adapter (TEST glue): wraps a scripted Value as a WireValue (+ WireRow/WireList reached
|
|
2283
|
+
// via as_row/as_list) and classifies each datum's native type into a DynamoDB-flavored wire tag
|
|
2284
|
+
// (S/N/BOOL/L/M/NULL). A real consumer implements WireValue over its own AttributeValue payload; this
|
|
2285
|
+
// drives the generated inline de-box from the same scripted vectors run_behavior uses — so the
|
|
2286
|
+
// actualWireType observed in a mismatch is the PRODUCER tag.
|
|
2705
2287
|
const wireAdapter = anyDeBox
|
|
2706
2288
|
? `// ── scripted wire adapter (TEST glue) ──
|
|
2707
2289
|
fn scripted_wire_tag(v: &Value) -> String {
|
|
@@ -2793,6 +2375,37 @@ pub struct ScriptedWireList {
|
|
|
2793
2375
|
items: Vec<Value>,
|
|
2794
2376
|
}
|
|
2795
2377
|
|
|
2378
|
+
// ScriptedWireValue — a scripted node result wrapped as the uniform WireValue: a top value is always
|
|
2379
|
+
// present (Some), so as_* never observes Absent; a scripted null is Value::Null → Probe::Null.
|
|
2380
|
+
pub struct ScriptedWireValue {
|
|
2381
|
+
v: Value,
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
impl ScriptedWireValue {
|
|
2385
|
+
pub fn new(v: Value) -> Self {
|
|
2386
|
+
ScriptedWireValue { v }
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
impl WireValue for ScriptedWireValue {
|
|
2391
|
+
type Row = ScriptedWireRow;
|
|
2392
|
+
fn as_string(&self) -> Probe<String> {
|
|
2393
|
+
probe_string_val(Some(&self.v))
|
|
2394
|
+
}
|
|
2395
|
+
fn as_number(&self) -> NumProbe {
|
|
2396
|
+
probe_number_val(Some(&self.v))
|
|
2397
|
+
}
|
|
2398
|
+
fn as_bool(&self) -> Probe<bool> {
|
|
2399
|
+
probe_bool_val(Some(&self.v))
|
|
2400
|
+
}
|
|
2401
|
+
fn as_row(&self) -> Probe<ScriptedWireRow> {
|
|
2402
|
+
probe_row_val(Some(&self.v))
|
|
2403
|
+
}
|
|
2404
|
+
fn as_list(&self) -> Probe<ScriptedWireList> {
|
|
2405
|
+
probe_list_val(Some(&self.v))
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2796
2409
|
impl WireRow for ScriptedWireRow {
|
|
2797
2410
|
type List = ScriptedWireList;
|
|
2798
2411
|
fn keys(&self) -> Vec<String> {
|
|
@@ -2837,10 +2450,8 @@ impl WireList for ScriptedWireList {
|
|
|
2837
2450
|
}
|
|
2838
2451
|
}`
|
|
2839
2452
|
: "";
|
|
2840
|
-
// ── concrete scripted handler: implements every HandlerNR<comp> trait by
|
|
2841
|
-
//
|
|
2842
|
-
const decodeFns = [];
|
|
2843
|
-
const emittedDecode = new Set();
|
|
2453
|
+
// ── concrete scripted handler: implements every HandlerNR<comp> trait by returning the scripted ok
|
|
2454
|
+
// Value as a WireValue (the generic scripted-source queue meets the uniform wire seam). ──
|
|
2844
2455
|
const traitImpls = [];
|
|
2845
2456
|
for (const c of native) {
|
|
2846
2457
|
const ap = asyncPlans.get(c.name);
|
|
@@ -2860,15 +2471,11 @@ impl WireList for ScriptedWireList {
|
|
|
2860
2471
|
// Option<K> (NOT a boxed Value) — computed per node so the impl signature matches the trait.
|
|
2861
2472
|
const bt = boundRustType(n, typedNodes, plan);
|
|
2862
2473
|
if ("fanout" in n) {
|
|
2474
|
+
// fanout: return the aligned per-id element WIRES (Vec<Option<ScriptedWireValue>>); a null aligned
|
|
2475
|
+
// body is a DANGLING id → None (the covered runner maps it to the zero element).
|
|
2863
2476
|
const f = n.fanout;
|
|
2864
|
-
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
2865
|
-
const elemT = rawElemStructName(c.name, n.id);
|
|
2866
|
-
const batchT = rawRowStructName(c.name, n.id);
|
|
2867
2477
|
const portsT = `${portsStructName(c.name, n.id)}Batch`;
|
|
2868
|
-
|
|
2869
|
-
// a record-element fanout returns the aligned element WIRES; a null aligned body is a DANGLING
|
|
2870
|
-
// id → None (the covered runner maps it to the zero elem row).
|
|
2871
|
-
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<Option<ScriptedWireRow>>, BehaviorError> {
|
|
2478
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<Option<ScriptedWireValue>>, BehaviorError> {
|
|
2872
2479
|
let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)}).ok_or_else(|| unknown_component(${rustStrLit(f.component)}))?;
|
|
2873
2480
|
if is_err {
|
|
2874
2481
|
return Err(leaf_failure(err));
|
|
@@ -2878,201 +2485,91 @@ impl WireList for ScriptedWireList {
|
|
|
2878
2485
|
.into_iter()
|
|
2879
2486
|
.map(|ev| match ev {
|
|
2880
2487
|
Value::Null => None,
|
|
2881
|
-
_ => Some(
|
|
2488
|
+
_ => Some(ScriptedWireValue::new(ev)),
|
|
2882
2489
|
})
|
|
2883
2490
|
.collect();
|
|
2884
2491
|
Ok(wires)
|
|
2885
|
-
}`);
|
|
2886
|
-
continue;
|
|
2887
|
-
}
|
|
2888
|
-
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
2889
|
-
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2890
|
-
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<${batchT}, BehaviorError> {
|
|
2891
|
-
let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)}).ok_or_else(|| unknown_component(${rustStrLit(f.component)}))?;
|
|
2892
|
-
if is_err {
|
|
2893
|
-
return Err(leaf_failure(err));
|
|
2894
|
-
}
|
|
2895
|
-
let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
|
|
2896
|
-
let rows = arr
|
|
2897
|
-
.iter()
|
|
2898
|
-
.map(|ev| match ev {
|
|
2899
|
-
Value::Null => ${elemT}::default(),
|
|
2900
|
-
_ => ${elemDecode}(ev),
|
|
2901
|
-
})
|
|
2902
|
-
.collect();
|
|
2903
|
-
Ok(${batchT} { rows })
|
|
2904
2492
|
}`);
|
|
2905
2493
|
continue;
|
|
2906
2494
|
}
|
|
2907
2495
|
if ("map" in n) {
|
|
2496
|
+
// map: batched → one WIRE per element (Vec<ScriptedWireValue>); non-batched → one element WIRE.
|
|
2908
2497
|
const m = n.map;
|
|
2909
2498
|
const batched = m.batched === true;
|
|
2910
|
-
const arrRef = ref;
|
|
2911
|
-
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
2912
|
-
const elemT = rawElemStructName(c.name, n.id);
|
|
2913
|
-
const batchT = rawRowStructName(c.name, n.id);
|
|
2914
2499
|
const portsT = batched ? `${portsStructName(c.name, n.id)}Batch` : portsStructName(c.name, n.id);
|
|
2915
|
-
if (elemDeBox) {
|
|
2916
|
-
// a record-element map returns the element WIRE(s); the covered runner de-boxes each strictly.
|
|
2917
|
-
if (batched) {
|
|
2918
|
-
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<ScriptedWireRow>, BehaviorError> {
|
|
2919
|
-
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
|
|
2920
|
-
if is_err {
|
|
2921
|
-
return Err(leaf_failure(err));
|
|
2922
|
-
}
|
|
2923
|
-
let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
|
|
2924
|
-
let wires = arr.into_iter().map(ScriptedWireRow::new).collect();
|
|
2925
|
-
Ok(wires)
|
|
2926
|
-
}`);
|
|
2927
|
-
}
|
|
2928
|
-
else {
|
|
2929
|
-
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<ScriptedWireRow, BehaviorError> {
|
|
2930
|
-
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
|
|
2931
|
-
if is_err {
|
|
2932
|
-
return Err(leaf_failure(err));
|
|
2933
|
-
}
|
|
2934
|
-
Ok(ScriptedWireRow::new(ok.unwrap_or(Value::Null)))
|
|
2935
|
-
}`);
|
|
2936
|
-
}
|
|
2937
|
-
continue;
|
|
2938
|
-
}
|
|
2939
|
-
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
2940
|
-
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2941
2500
|
if (batched) {
|
|
2942
|
-
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result
|
|
2501
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<ScriptedWireValue>, BehaviorError> {
|
|
2943
2502
|
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
|
|
2944
2503
|
if is_err {
|
|
2945
2504
|
return Err(leaf_failure(err));
|
|
2946
2505
|
}
|
|
2947
2506
|
let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
|
|
2948
|
-
let
|
|
2949
|
-
Ok(
|
|
2507
|
+
let wires = arr.into_iter().map(ScriptedWireValue::new).collect();
|
|
2508
|
+
Ok(wires)
|
|
2950
2509
|
}`);
|
|
2951
2510
|
}
|
|
2952
2511
|
else {
|
|
2953
|
-
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result
|
|
2512
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<ScriptedWireValue, BehaviorError> {
|
|
2954
2513
|
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
|
|
2955
2514
|
if is_err {
|
|
2956
2515
|
return Err(leaf_failure(err));
|
|
2957
2516
|
}
|
|
2958
|
-
|
|
2959
|
-
Ok(${elemDecode}(&v))
|
|
2517
|
+
Ok(ScriptedWireValue::new(ok.unwrap_or(Value::Null)))
|
|
2960
2518
|
}`);
|
|
2961
2519
|
}
|
|
2962
2520
|
continue;
|
|
2963
2521
|
}
|
|
2522
|
+
// componentRef: return the whole result WIRE (one ScriptedWireValue wrapping the scripted ok Value);
|
|
2523
|
+
// the covered runner's INLINE de-box classifies it. A real consumer returns its own wire payload.
|
|
2964
2524
|
const node = n;
|
|
2965
|
-
|
|
2966
|
-
//
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
case "named":
|
|
2978
|
-
return "ScriptedWireRow";
|
|
2979
|
-
case "arr":
|
|
2980
|
-
return "Vec<ScriptedWireRow>";
|
|
2981
|
-
case "opt":
|
|
2982
|
-
return "Option<ScriptedWireRow>";
|
|
2983
|
-
case "map":
|
|
2984
|
-
return "std::collections::BTreeMap<String, ScriptedWireRow>";
|
|
2525
|
+
// echo (#129/#141 fixtures): the scripted leaf can ECHO an input PORT back as its result wire, so the
|
|
2526
|
+
// input-port value flows into the de-boxed result and the runtime non-vacuity asserts survive.
|
|
2527
|
+
const echoBranches = [];
|
|
2528
|
+
if (ref.kind === "named") {
|
|
2529
|
+
const decl = findDecl(plan, ref.name);
|
|
2530
|
+
const field = decl !== undefined && decl.fields.length === 1 ? decl.fields[0] : undefined;
|
|
2531
|
+
const fieldRust = field !== undefined ? renderTypeRef(field.type) : null;
|
|
2532
|
+
const recordRust = renderTypeRef(ref);
|
|
2533
|
+
for (const pn of Object.keys(node.ports)) {
|
|
2534
|
+
let portRust = null;
|
|
2535
|
+
try {
|
|
2536
|
+
portRust = portFieldRustType(node.ports[pn], c, plan, typedNodes, `echo port '${pn}'`);
|
|
2985
2537
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
let
|
|
2998
|
-
try {
|
|
2999
|
-
portRust = portFieldRustType(node.ports[pn], c, plan, typedNodes, `echo port '${pn}'`);
|
|
3000
|
-
}
|
|
3001
|
-
catch {
|
|
3002
|
-
portRust = null;
|
|
3003
|
-
}
|
|
3004
|
-
if (portRust === recordRust) {
|
|
3005
|
-
// case 2: the port IS the whole record → echo it DIRECTLY (no field wrap), matching
|
|
3006
|
-
// run_behavior's plain echo:port of a whole-record port.
|
|
3007
|
-
const own = typeRefIsCopy(ref) ? `ports.${portFieldName(pn)}` : `ports.${portFieldName(pn)}.clone()`;
|
|
3008
|
-
namedEcho += ` if echo == ${rustStrLit(pn)} {\n let tv = ${own};\n return Ok(ScriptedWireRow::new(${serializeTyped("tv", ref, plan)}));\n }\n`;
|
|
3009
|
-
}
|
|
3010
|
-
else if (fieldRust !== null && field !== undefined && portRust === fieldRust) {
|
|
3011
|
-
// case 1: a scalar port wraps into the record's single field ({v: ports.X}).
|
|
3012
|
-
const own = typeRefIsCopy(field.type) ? `ports.${portFieldName(pn)}` : `ports.${portFieldName(pn)}.clone()`;
|
|
3013
|
-
namedEcho += ` if echo == ${rustStrLit(pn)} {\n let tv = ${own};\n return Ok(ScriptedWireRow::new(Value::Obj(vec![(${rustStrLit(field.name)}.to_string(), ${serializeTyped("tv", field.type, plan)})])));\n }\n`;
|
|
3014
|
-
}
|
|
2538
|
+
catch {
|
|
2539
|
+
portRust = null;
|
|
2540
|
+
}
|
|
2541
|
+
if (portRust === recordRust) {
|
|
2542
|
+
// the port IS the whole record → echo it DIRECTLY (matching run_behavior's plain echo:port).
|
|
2543
|
+
const own = typeRefIsCopy(ref) ? `ports.${portFieldName(pn)}` : `ports.${portFieldName(pn)}.clone()`;
|
|
2544
|
+
echoBranches.push(` if echo == ${rustStrLit(pn)} {\n let tv = ${own};\n return Ok(ScriptedWireValue::new(${serializeTyped("tv", ref, plan)}));\n }`);
|
|
2545
|
+
}
|
|
2546
|
+
else if (fieldRust !== null && field !== undefined && portRust === fieldRust) {
|
|
2547
|
+
// a scalar port wraps into the record's single field ({v: ports.X}).
|
|
2548
|
+
const own = typeRefIsCopy(field.type) ? `ports.${portFieldName(pn)}` : `ports.${portFieldName(pn)}.clone()`;
|
|
2549
|
+
echoBranches.push(` if echo == ${rustStrLit(pn)} {\n let tv = ${own};\n return Ok(ScriptedWireValue::new(Value::Obj(vec![(${rustStrLit(field.name)}.to_string(), ${serializeTyped("tv", field.type, plan)})])));\n }`);
|
|
3015
2550
|
}
|
|
3016
2551
|
}
|
|
3017
|
-
const canEcho = (top.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items")) || namedEcho !== "";
|
|
3018
|
-
const portsParam = canEcho ? "ports" : "_ports";
|
|
3019
|
-
const okBind = canEcho ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
|
|
3020
|
-
let body;
|
|
3021
|
-
if (top.kind === "named") {
|
|
3022
|
-
body = `${namedEcho} Ok(ScriptedWireRow::new(ok.unwrap_or(Value::Null)))`;
|
|
3023
|
-
}
|
|
3024
|
-
else if (top.kind === "arr") {
|
|
3025
|
-
const elemRef = ref.elem;
|
|
3026
|
-
const itemsF = portFieldName("items");
|
|
3027
|
-
const echoBranch = canEcho
|
|
3028
|
-
? ` if echo == "items" {\n let wires: Vec<ScriptedWireRow> = ports.${itemsF}.iter().map(|e| { let tv = e.clone(); ScriptedWireRow::new(${serializeTyped("tv", elemRef, plan)}) }).collect();\n return Ok(wires);\n }\n`
|
|
3029
|
-
: "";
|
|
3030
|
-
body = `${echoBranch} let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };\n Ok(arr.into_iter().map(ScriptedWireRow::new).collect())`;
|
|
3031
|
-
}
|
|
3032
|
-
else if (top.kind === "opt") {
|
|
3033
|
-
body = ` match ok {\n None | Some(Value::Null) => Ok(None),\n Some(v) => Ok(Some(ScriptedWireRow::new(v))),\n }`;
|
|
3034
|
-
}
|
|
3035
|
-
else {
|
|
3036
|
-
body = ` let obj = match ok { Some(Value::Obj(pairs)) => pairs, _ => Vec::new() };\n Ok(obj.into_iter().map(|(k, v)| (k, ScriptedWireRow::new(v))).collect())`;
|
|
3037
|
-
}
|
|
3038
|
-
methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Result<${wireT}, BehaviorError> {
|
|
3039
|
-
let ${okBind} = self.next(${rustStrLit(node.component)}).ok_or_else(|| unknown_component(${rustStrLit(node.component)}))?;
|
|
3040
|
-
if is_err {
|
|
3041
|
-
return Err(leaf_failure(err));
|
|
3042
|
-
}
|
|
3043
|
-
${body}
|
|
3044
|
-
}`);
|
|
3045
|
-
continue;
|
|
3046
2552
|
}
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
const canEcho =
|
|
2553
|
+
else if (ref.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items")) {
|
|
2554
|
+
// #129: an arr node with an `items` port echoes the NATIVE port array (serialized element-by-element
|
|
2555
|
+
// into a Value::Arr) so the node→leaf array dataflow test is non-vacuous.
|
|
2556
|
+
const elemRef = ref.elem;
|
|
2557
|
+
echoBranches.push(` if echo == "items" {\n let tv = Value::Arr(ports.${portFieldName("items")}.iter().map(|e| { let ev = e.clone(); ${serializeTyped("ev", elemRef, plan)} }).collect());\n return Ok(ScriptedWireValue::new(tv));\n }`);
|
|
2558
|
+
}
|
|
2559
|
+
const canEcho = echoBranches.length > 0;
|
|
3054
2560
|
const portsParam = canEcho ? "ports" : "_ports";
|
|
3055
|
-
const echoBranch = canEcho
|
|
3056
|
-
? `\n if echo == "items" {\n return Ok(${rowT} { val: ${portsParam}.${portFieldName("items")}.clone() });\n }`
|
|
3057
|
-
: "";
|
|
3058
2561
|
const okBind = canEcho ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
|
|
3059
|
-
|
|
2562
|
+
const echoBlock = canEcho ? `${echoBranches.join("\n")}\n` : "";
|
|
2563
|
+
methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Result<ScriptedWireValue, BehaviorError> {
|
|
3060
2564
|
let ${okBind} = self.next(${rustStrLit(node.component)}).ok_or_else(|| unknown_component(${rustStrLit(node.component)}))?;
|
|
3061
2565
|
if is_err {
|
|
3062
2566
|
return Err(leaf_failure(err));
|
|
3063
|
-
}
|
|
3064
|
-
|
|
3065
|
-
Ok(${decode}(&v))
|
|
2567
|
+
}
|
|
2568
|
+
${echoBlock} Ok(ScriptedWireValue::new(ok.unwrap_or(Value::Null)))
|
|
3066
2569
|
}`);
|
|
3067
2570
|
}
|
|
3068
|
-
const
|
|
3069
|
-
|
|
3070
|
-
return false;
|
|
3071
|
-
if ("map" in n || "fanout" in n)
|
|
3072
|
-
return mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
3073
|
-
return deBoxEligible(typedNodes.get(n.id));
|
|
3074
|
-
});
|
|
3075
|
-
const wireAssoc = cHasDeBox ? ` type Wire = ScriptedWireRow;\n` : "";
|
|
2571
|
+
const cHasHandler = c.body.some((n) => !("cond" in n));
|
|
2572
|
+
const wireAssoc = cHasHandler ? ` type Wire = ScriptedWireValue;\n` : "";
|
|
3076
2573
|
traitImpls.push(`impl ${handlerTraitName(c.name)} for ScriptedNativeRaw {\n${wireAssoc}${methods.join("\n")}\n}`);
|
|
3077
2574
|
}
|
|
3078
2575
|
const arms = native
|
|
@@ -3218,13 +2715,10 @@ ${marshallers}
|
|
|
3218
2715
|
|
|
3219
2716
|
${emitMarshalHelpers()}
|
|
3220
2717
|
|
|
3221
|
-
// per-node concrete-row decoders (scripted Value -> RawRowNR_/RawElemNR_ struct).
|
|
3222
|
-
${decodeFns.join("\n\n")}
|
|
3223
|
-
|
|
3224
2718
|
// input decoders (generic &[(String,Value)] -> concrete InNR_<comp>; TEST glue, one decode at the boundary).
|
|
3225
2719
|
${inDecoders}
|
|
3226
2720
|
|
|
3227
|
-
// scripted wire adapter (TEST glue) — wraps a scripted Value as a
|
|
2721
|
+
// scripted wire adapter (TEST glue) — wraps a scripted Value as a WireValue the generated inline de-box probes.
|
|
3228
2722
|
${wireAdapter}
|
|
3229
2723
|
|
|
3230
2724
|
${adapter}
|
|
@@ -3315,31 +2809,6 @@ function emitInDecoder(comp, plan) {
|
|
|
3315
2809
|
lines.push(`}`);
|
|
3316
2810
|
return lines.join("\n");
|
|
3317
2811
|
}
|
|
3318
|
-
/** emitDecodeRow — emit (once, deduped) a decoder `decode_row_<comp>_<node><suffix>(v: &Value) -> <rowT>`
|
|
3319
|
-
* that materializes the scripted ok Value into the concrete row struct. Named: reuse the Value->struct
|
|
3320
|
-
* marshaller + move fields; scalar/arr/opt: materialize into .val. TEST glue only. */
|
|
3321
|
-
function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix) {
|
|
3322
|
-
const fn = `decode_row_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}${suffix.toLowerCase()}`;
|
|
3323
|
-
if (emitted.has(fn))
|
|
3324
|
-
return fn;
|
|
3325
|
-
emitted.add(fn);
|
|
3326
|
-
const lines = [];
|
|
3327
|
-
lines.push(`fn ${fn}(v: &Value) -> ${rowT} {`);
|
|
3328
|
-
if (ref.kind === "named") {
|
|
3329
|
-
const decl = findDecl(plan, ref.name);
|
|
3330
|
-
lines.push(` let t = marshal_${ref.name}(v).expect("scripted row decode");`);
|
|
3331
|
-
const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: t.${rustFieldName(f.name)}`).join(", ");
|
|
3332
|
-
lines.push(` ${rowT} { ${inits} }`);
|
|
3333
|
-
}
|
|
3334
|
-
else {
|
|
3335
|
-
const mat = materializeExpr("v", ref, plan);
|
|
3336
|
-
lines.push(` let val = (|| -> Result<${renderTypeRef(ref)}, BehaviorError> { Ok(${mat}) })().expect("scripted row decode");`);
|
|
3337
|
-
lines.push(` ${rowT} { val }`);
|
|
3338
|
-
}
|
|
3339
|
-
lines.push(`}`);
|
|
3340
|
-
out.push(lines.join("\n"));
|
|
3341
|
-
return fn;
|
|
3342
|
-
}
|
|
3343
2812
|
export const rustTypedNativeEmitter = {
|
|
3344
2813
|
language: "rust-typed-native",
|
|
3345
2814
|
classification: "codegen", // native: 型付きフラットコード生成・IR/dict 非在(#128/A6)
|