behavior-contracts 0.7.1 → 0.7.3
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 +70 -1
- package/dist/authoring.js.map +1 -1
- package/dist/behavior.d.ts +79 -2
- package/dist/behavior.d.ts.map +1 -1
- package/dist/behavior.js +103 -3
- package/dist/behavior.js.map +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +67 -4
- package/dist/builder.js.map +1 -1
- package/dist/generator/emit-shared-rust-typed.d.ts.map +1 -1
- package/dist/generator/emit-shared-rust-typed.js +9 -0
- package/dist/generator/emit-shared-rust-typed.js.map +1 -1
- package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-go.js +281 -2
- 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 +265 -2
- package/dist/generator/emit-typed-native-rust.js.map +1 -1
- package/dist/generator/straightline.d.ts.map +1 -1
- package/dist/generator/straightline.js +21 -1
- package/dist/generator/straightline.js.map +1 -1
- package/dist/guard.js +20 -1
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -322,6 +322,39 @@ function coveredMapNode(n, bodyIds, comp) {
|
|
|
322
322
|
return false;
|
|
323
323
|
return true;
|
|
324
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* coveredFanoutNode (v3) — a FANOUT node (first-class `@refs` id-list fan-out) is covered when:
|
|
327
|
+
* - over is a static ref to a PRIOR BODY NODE's typed arr field OR an INPUT array port with elemType,
|
|
328
|
+
* - every element port is static (literal / ref into the $as binding / concat / static arr),
|
|
329
|
+
* - default 'fail' policy, relationKind connection (guard-checked by the guard/builder).
|
|
330
|
+
* The fanout de-boxes to: one deduped batched dispatch → first-seen dedupe (by the elem's dedupeKey
|
|
331
|
+
* field) + dangling drop + implicitSource strip + connection wrap {items, cursor:nil} — NO boxed Value.
|
|
332
|
+
*/
|
|
333
|
+
function coveredFanoutNode(n, bodyIds, comp) {
|
|
334
|
+
if (n === null || typeof n !== "object" || !("fanout" in n))
|
|
335
|
+
return false;
|
|
336
|
+
const f = n.fanout;
|
|
337
|
+
if (f.policy !== undefined && f.policy !== "fail")
|
|
338
|
+
return false;
|
|
339
|
+
if (f.relationKind !== "connection")
|
|
340
|
+
return false;
|
|
341
|
+
const overE = classifyExpr(f.over);
|
|
342
|
+
if (overE.kind !== "ref")
|
|
343
|
+
return false;
|
|
344
|
+
const head = overE.path[0];
|
|
345
|
+
if (!bodyIds.has(head)) {
|
|
346
|
+
if (overE.path.length !== 1)
|
|
347
|
+
return false;
|
|
348
|
+
const schema = comp.inputPorts?.[head];
|
|
349
|
+
if (schema === undefined || schema.elemType === undefined)
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
const ports = f.ports;
|
|
353
|
+
for (const p of Object.keys(ports))
|
|
354
|
+
if (!portIsStatic(ports[p]))
|
|
355
|
+
return false;
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
325
358
|
/** A covered read SHAPE (typing-independent) — sequential componentRef reads + covered map...into,
|
|
326
359
|
* OR a real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point
|
|
327
360
|
* reads. A real-concurrency plan whose parallel members include a map / bindField-relation child is
|
|
@@ -366,6 +399,13 @@ function coverageRejectReason(comp) {
|
|
|
366
399
|
return `node '${n.id}' cond has no outType (required to de-box the branch join)`;
|
|
367
400
|
continue;
|
|
368
401
|
}
|
|
402
|
+
if ("fanout" in n) {
|
|
403
|
+
// v3: a first-class fanout node (deduped connection list-in fan-out) IS covered when its over is a
|
|
404
|
+
// covered ref (prior-node arr / input array with elemType) and its element ports are static.
|
|
405
|
+
if (!coveredFanoutNode(n, bodyIds, comp))
|
|
406
|
+
return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
369
409
|
if ("map" in n) {
|
|
370
410
|
// #86 pt2 batched map...into / #93 shape 2/3 / #108 guarded map (no into) / map-over-input (with
|
|
371
411
|
// elemType) ARE covered. A non-covered map shape (guard+into heterogeneous / over-input without
|
|
@@ -486,6 +526,15 @@ function emitRawRowStructs(comp, typedNodes, plan) {
|
|
|
486
526
|
if ("cond" in n)
|
|
487
527
|
continue;
|
|
488
528
|
const ref = typedNodes.get(n.id);
|
|
529
|
+
if ("fanout" in n) {
|
|
530
|
+
// a covered fanout node: RawElem_ is the connection items element (the per-body row the batched
|
|
531
|
+
// handler yields, aligned to the deduped id list); the batch RawRow_ carries the aligned []RawElem_.
|
|
532
|
+
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
533
|
+
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
534
|
+
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
|
|
535
|
+
`type ${rawRowStructName(comp.name, n.id)} struct {\n\tIsError bool\n\tErr string\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
489
538
|
if ("map" in n) {
|
|
490
539
|
// a covered map node: RawElem_<comp>_<node> is the HANDLER'S per-element return shape:
|
|
491
540
|
// - into: the `into` field's type (the fetched item merged onto every over element);
|
|
@@ -583,6 +632,14 @@ function emitHandlerInterface(comp, typedNodes, plan) {
|
|
|
583
632
|
continue; // #108: a cond node has no handler method (pure Expression).
|
|
584
633
|
const method = nodeMethodName(comp.name, n.id);
|
|
585
634
|
const bt = boundGoType(n, comp, typedNodes, plan);
|
|
635
|
+
if ("fanout" in n) {
|
|
636
|
+
// fanout (v3): one batched dispatch of the deduped id list. Ports = the batch struct; returns the
|
|
637
|
+
// aligned batch row (per-body rows) + resolved. The runner applies dedupe/drop/wrap after.
|
|
638
|
+
const portsT = `${portsStructName(comp.name, n.id)}_batch`;
|
|
639
|
+
const retT = rawRowStructName(comp.name, n.id);
|
|
640
|
+
lines.push(`\t${method}(ports ${portsT}, bound ${bt}) (${retT}, bool)`);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
586
643
|
if ("map" in n) {
|
|
587
644
|
const m = n.map;
|
|
588
645
|
const batched = m.batched === true;
|
|
@@ -626,6 +683,59 @@ type ${batchStruct} struct {
|
|
|
626
683
|
Items []${structName}
|
|
627
684
|
}`;
|
|
628
685
|
}
|
|
686
|
+
/** emitFanoutPortsStructs (v3) — the per-body element ports struct + the batch struct for a covered
|
|
687
|
+
* fanout node (identical shape to a batched map's ports). The element ports read the `$as` id binding. */
|
|
688
|
+
function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
|
|
689
|
+
const f = node.fanout;
|
|
690
|
+
const structName = portsStructName(comp.name, node.id);
|
|
691
|
+
const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
|
|
692
|
+
const asBinding = { name: f.as, ref: elemRef, plan };
|
|
693
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan);
|
|
694
|
+
const batchStruct = `${structName}_batch`;
|
|
695
|
+
return `${elemStruct}
|
|
696
|
+
|
|
697
|
+
// ${batchStruct} — NATIVE batched ports for fanout '${node.id}': Items is a native slice of the concrete
|
|
698
|
+
// per-id ports structs (${structName}). The consumer reads Items[i].<Field> DIRECTLY (bc#90 / runtime-free).
|
|
699
|
+
type ${batchStruct} struct {
|
|
700
|
+
Items []${structName}
|
|
701
|
+
}`;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* fanoutItemsElemRef (v3) — the connection items ELEMENT TypeRef for a covered fanout node. The fanout
|
|
705
|
+
* outType is a connection struct `{items:[]Elem, cursor:*string}`; this returns Elem (the per-body row
|
|
706
|
+
* the batched handler yields, aligned to the deduped id list, before dedupe/drop). Fails closed if the
|
|
707
|
+
* outType is not that connection shape (named struct with an `items` arr field).
|
|
708
|
+
*/
|
|
709
|
+
function fanoutItemsElemRef(node, connRef, plan) {
|
|
710
|
+
if (connRef.kind !== "named") {
|
|
711
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go fanout (v3): node '${node.id}' outType must be a connection struct {items,cursor} (got ${JSON.stringify(connRef.kind)}).`);
|
|
712
|
+
}
|
|
713
|
+
const decl = findDecl(plan, connRef.name);
|
|
714
|
+
const itemsField = decl.fields.find((fd) => fd.name === "items");
|
|
715
|
+
if (!itemsField || itemsField.type.kind !== "arr") {
|
|
716
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go fanout (v3): node '${node.id}' connection struct '${connRef.name}' has no 'items' array field.`);
|
|
717
|
+
}
|
|
718
|
+
return itemsField.type.elem;
|
|
719
|
+
}
|
|
720
|
+
/** fanoutOverElemInfo (v3) — the over id-list element TypeRef + the native Go over-slice expression.
|
|
721
|
+
* Mirrors mapOverElemInfo but reads the FanoutNode.fanout spec (prior-node arr OR input array w/ elemType). */
|
|
722
|
+
function fanoutOverElemInfo(comp, node, typedNodes, plan) {
|
|
723
|
+
const f = node.fanout;
|
|
724
|
+
const e = classifyExpr(f.over);
|
|
725
|
+
if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
|
|
726
|
+
const baseRef = typedNodes.get(e.path[0]);
|
|
727
|
+
const { expr, ref } = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
|
|
728
|
+
if (ref.kind === "arr")
|
|
729
|
+
return { elemRef: ref.elem, overExpr: expr };
|
|
730
|
+
}
|
|
731
|
+
if (e.kind === "ref" && e.path.length === 1) {
|
|
732
|
+
const schema = comp.inputPorts?.[e.path[0]];
|
|
733
|
+
const et = schema?.elemType;
|
|
734
|
+
if (et !== undefined)
|
|
735
|
+
return { elemRef: deriveTypeRef(et, plan), overExpr: `in.${goFieldName(e.path[0])}` };
|
|
736
|
+
}
|
|
737
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go fanout (v3): fanout '${node.id}' 'over' must be a ref to a prior node's typed arr field OR an input array port with a declared elemType (got ${JSON.stringify(f.over)}).`);
|
|
738
|
+
}
|
|
629
739
|
/** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
|
|
630
740
|
* type (into) or the element outType directly (no-into). This is the type of RawElem_<comp>_<node>. */
|
|
631
741
|
function mapElemRowRef(node, arrRef, plan) {
|
|
@@ -1220,6 +1330,12 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1220
1330
|
k += stage.length - 1;
|
|
1221
1331
|
continue;
|
|
1222
1332
|
}
|
|
1333
|
+
// v3: a covered fanout node — deduped batched dispatch + first-seen dedupe + dangling drop +
|
|
1334
|
+
// connection wrap (the SAME definition as run_behavior's fanoutDedupDrop).
|
|
1335
|
+
if ("fanout" in comp.body[i]) {
|
|
1336
|
+
lines.push(emitFanoutArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut));
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1223
1339
|
// #86 pt2: a covered batched map...into node — struct-native element collection + into enrichment.
|
|
1224
1340
|
if ("map" in comp.body[i]) {
|
|
1225
1341
|
lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut, flags));
|
|
@@ -1536,6 +1652,132 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1536
1652
|
function elemLocal(s) {
|
|
1537
1653
|
return `oel_${s}`;
|
|
1538
1654
|
}
|
|
1655
|
+
/**
|
|
1656
|
+
* emitFanoutArm (v3) — the struct-native exec of a covered FANOUT node. This is the genuinely new
|
|
1657
|
+
* native list-in fan-out handler: build the per-id native ports (all over ids), dispatch the batched
|
|
1658
|
+
* handler ONCE (aligned per-body rows), then apply THE SAME dedupe/drop as run_behavior's
|
|
1659
|
+
* fanoutDedupDrop — first-seen dedupe by the elem's dedupeKey field, drop dangling (a body whose
|
|
1660
|
+
* dedupeKey field is the zero value ≡ the interpreter's null/absent-key body), implicitSource strip
|
|
1661
|
+
* (STRUCTURAL — the connection items elem type already omits the implicitSource field), and wrap into
|
|
1662
|
+
* the connection struct {items, cursor:nil}. NO boxed Value on any plane. Byte-equal to run_behavior.
|
|
1663
|
+
*/
|
|
1664
|
+
function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut) {
|
|
1665
|
+
const f = node.fanout;
|
|
1666
|
+
const s = sanitize(node.id);
|
|
1667
|
+
const structName = portsStructName(comp.name, node.id);
|
|
1668
|
+
const batchStruct = `${structName}_batch`;
|
|
1669
|
+
const connRef = typedNodes.get(node.id);
|
|
1670
|
+
const elemRef = fanoutItemsElemRef(node, connRef, plan);
|
|
1671
|
+
const elemGo = renderTypeRef(elemRef);
|
|
1672
|
+
const { elemRef: overElemRef, overExpr } = fanoutOverElemInfo(comp, node, typedNodes, plan);
|
|
1673
|
+
const asName = f.as;
|
|
1674
|
+
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1675
|
+
const portNames = Object.keys(f.ports);
|
|
1676
|
+
// the dedupeKey Go field on the connection items elem (must be a string scalar for the seen-set key).
|
|
1677
|
+
if (elemRef.kind !== "named")
|
|
1678
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go fanout (v3): node '${node.id}' connection items element must be a named struct (got ${JSON.stringify(elemRef.kind)}).`);
|
|
1679
|
+
const elemDecl = findDecl(plan, elemRef.name);
|
|
1680
|
+
const dkField = elemDecl.fields.find((fd) => fd.name === f.dedupeKey);
|
|
1681
|
+
if (!dkField || dkField.type.kind !== "scalar" || dkField.type.scalar !== "string")
|
|
1682
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go fanout (v3): node '${node.id}' dedupeKey '${f.dedupeKey}' must be a string field of the connection items element type '${elemRef.name}'.`);
|
|
1683
|
+
const dkGo = goFieldName(f.dedupeKey);
|
|
1684
|
+
// per-id native ports build (same as a batched map's per-element ports). $as → the over id value.
|
|
1685
|
+
const buildPorts = (il, elemVar) => {
|
|
1686
|
+
const inits = [];
|
|
1687
|
+
const resolveRef = (head, restPath, opt) => {
|
|
1688
|
+
if (opt)
|
|
1689
|
+
return null;
|
|
1690
|
+
let baseExpr;
|
|
1691
|
+
let baseRef;
|
|
1692
|
+
if (head === asName) {
|
|
1693
|
+
baseExpr = elemVar;
|
|
1694
|
+
baseRef = overElemRef;
|
|
1695
|
+
}
|
|
1696
|
+
else if (priorHere(head)) {
|
|
1697
|
+
baseExpr = typedLocal(head);
|
|
1698
|
+
baseRef = typedNodes.get(head);
|
|
1699
|
+
}
|
|
1700
|
+
else {
|
|
1701
|
+
if (restPath.length !== 0)
|
|
1702
|
+
return null;
|
|
1703
|
+
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1704
|
+
if (scalar === undefined)
|
|
1705
|
+
return null;
|
|
1706
|
+
return { expr: `in.${goFieldName(head)}`, scalar };
|
|
1707
|
+
}
|
|
1708
|
+
let acc;
|
|
1709
|
+
let expr;
|
|
1710
|
+
try {
|
|
1711
|
+
const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
|
|
1712
|
+
expr = r.expr;
|
|
1713
|
+
acc = r.ref;
|
|
1714
|
+
}
|
|
1715
|
+
catch {
|
|
1716
|
+
return null;
|
|
1717
|
+
}
|
|
1718
|
+
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1719
|
+
return null;
|
|
1720
|
+
return { expr, scalar: goScalar(acc.scalar) };
|
|
1721
|
+
};
|
|
1722
|
+
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1723
|
+
for (const pn of portNames) {
|
|
1724
|
+
const fieldGoType = portFieldGoType(f.ports[pn], comp.inputPorts, `fanout port '${pn}' of node '${node.id}'`, asBinding, plan);
|
|
1725
|
+
inits.push(emitPortInit(pn, f.ports[pn], fieldGoType, resolveRef, comp.inputPorts, plan));
|
|
1726
|
+
}
|
|
1727
|
+
return [`${il}ep_${s} := ${structName}{${inits.join(", ")}}`];
|
|
1728
|
+
};
|
|
1729
|
+
const lines = [];
|
|
1730
|
+
const closers = [];
|
|
1731
|
+
lines.push(`\t// ── fanout '${node.id}' (${f.component}, dedupeKey:${JSON.stringify(f.dedupeKey)}, drop:${JSON.stringify(f.drop)}${f.implicitSource ? ", implicitSource:" + JSON.stringify(f.implicitSource) : ""}${f.parent ? ", parent:" + f.parent : ""}) ──`);
|
|
1732
|
+
let indent = "\t";
|
|
1733
|
+
if (f.parent !== undefined && priorNodeAt(f.parent, atPos)) {
|
|
1734
|
+
lines.push(`${indent}if produced_${sanitize(f.parent)} {`);
|
|
1735
|
+
closers.unshift(`${indent}}`);
|
|
1736
|
+
indent += "\t";
|
|
1737
|
+
}
|
|
1738
|
+
// over id-list (typed field access — no serialize) → per-id ports (all ids; dedupe is on the result).
|
|
1739
|
+
lines.push(`${indent}over_${s} := ${overExpr}`);
|
|
1740
|
+
lines.push(`${indent}var items_${s} []${elemGo}`);
|
|
1741
|
+
lines.push(`${indent}pi_${s} := make([]${structName}, 0, len(over_${s}))`);
|
|
1742
|
+
lines.push(`${indent}for _, ${elemLocal(s)} := range over_${s} {`);
|
|
1743
|
+
for (const l of buildPorts(indent + "\t", elemLocal(s)))
|
|
1744
|
+
lines.push(l);
|
|
1745
|
+
lines.push(`${indent}\tpi_${s} = append(pi_${s}, ep_${s})`);
|
|
1746
|
+
lines.push(`${indent}}`);
|
|
1747
|
+
// empty id-list => empty connection (handler not called), matching run_behavior.
|
|
1748
|
+
lines.push(`${indent}if len(pi_${s}) > 0 {`);
|
|
1749
|
+
lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: pi_${s}}`);
|
|
1750
|
+
lines.push(`${indent}\tfo_${s}, fo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
|
|
1751
|
+
lines.push(`${indent}\tif !fo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${f.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
|
|
1752
|
+
lines.push(`${indent}\tif fo_${s}.IsError {`, `${indent}\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under 'fail' policy: "+fo_${s}.Err`)}`, `${indent}\t}`);
|
|
1753
|
+
lines.push(`${indent}\tif len(fo_${s}.Rows) != len(pi_${s}) {`, `${indent}\t\treturn ${zeroOut}, ${goErr("FANOUT_BATCH_RESULT_MISMATCH", `fmt.Sprintf("fanout '${node.id}': batched handler must return a list aligned to the deduped id list (want %d)", len(pi_${s}))`)}`, `${indent}\t}`);
|
|
1754
|
+
// ── THE ONE dedupe/drop (mirrors fanoutDedupDrop verbatim): first-seen dedupe by dedupeKey field;
|
|
1755
|
+
// drop dangling (zero dedupeKey ≡ interpreter's null/absent-key body); implicitSource strip is
|
|
1756
|
+
// structural (the elem type already omits it). cursor is always nil (connection wrap).
|
|
1757
|
+
lines.push(`${indent}\tseen_${s} := make(map[string]struct{}, len(fo_${s}.Rows))`);
|
|
1758
|
+
lines.push(`${indent}\titems_${s} = make([]${elemGo}, 0, len(fo_${s}.Rows))`);
|
|
1759
|
+
lines.push(`${indent}\tfor fi_${s} := range fo_${s}.Rows {`);
|
|
1760
|
+
lines.push(`${indent}\t\tbody_${s} := fo_${s}.Rows[fi_${s}]`);
|
|
1761
|
+
lines.push(`${indent}\t\tvar el_${s} ${elemGo}`);
|
|
1762
|
+
lines.push(emitRowCopy(`el_${s}`, `body_${s}`, elemRef, plan, indent.length + 2));
|
|
1763
|
+
lines.push(`${indent}\t\tkey_${s} := el_${s}.${dkGo}`);
|
|
1764
|
+
if (f.drop === "dangling") {
|
|
1765
|
+
// dangling: a body whose dedupeKey is the zero value (empty string) is dropped.
|
|
1766
|
+
lines.push(`${indent}\t\tif key_${s} == "" {`, `${indent}\t\t\tcontinue`, `${indent}\t\t}`);
|
|
1767
|
+
}
|
|
1768
|
+
lines.push(`${indent}\t\tif _, dup_${s} := seen_${s}[key_${s}]; dup_${s} {`, `${indent}\t\t\tcontinue`, `${indent}\t\t}`);
|
|
1769
|
+
lines.push(`${indent}\t\tseen_${s}[key_${s}] = struct{}{}`);
|
|
1770
|
+
lines.push(`${indent}\t\titems_${s} = append(items_${s}, el_${s})`);
|
|
1771
|
+
lines.push(`${indent}\t}`);
|
|
1772
|
+
lines.push(`${indent}}`);
|
|
1773
|
+
// wrap into the connection struct {Items, Cursor:nil}. A nil items slice serializes to [] (the
|
|
1774
|
+
// connection serializer normalizes it) — matching {items:[],cursor:null} for an empty fan-out.
|
|
1775
|
+
lines.push(`${indent}${typedLocal(node.id)} = ${connRef.name}{${goFieldName("items")}: items_${s}}`);
|
|
1776
|
+
lines.push(`${indent}produced_${s} = true`);
|
|
1777
|
+
for (const c of closers)
|
|
1778
|
+
lines.push(c);
|
|
1779
|
+
return lines.join("\n");
|
|
1780
|
+
}
|
|
1539
1781
|
/** #108: resolve an INPUT port head for a native-expr ref (cond `if` / map `when` / branches). A scalar
|
|
1540
1782
|
* port → `in.<Field>` of the scalar type; an array port WITH elemType → `in.<Field>` of `[]ElemT` (so
|
|
1541
1783
|
* `len(in.<Field>)` works); anything else → null (fail-closed). */
|
|
@@ -1974,7 +2216,11 @@ function emit(ctx) {
|
|
|
1974
2216
|
for (const n of c.body)
|
|
1975
2217
|
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1976
2218
|
for (const n of c.body) {
|
|
1977
|
-
if ("
|
|
2219
|
+
if ("fanout" in n) {
|
|
2220
|
+
// v3: a fanout node emits a per-id element ports struct + a batch struct (like a batched map).
|
|
2221
|
+
structs.push(emitFanoutPortsStructs(c, n, typedNodes, plan));
|
|
2222
|
+
}
|
|
2223
|
+
else if ("map" in n) {
|
|
1978
2224
|
// #86 pt2 / #108: a map node emits an element ports struct + a batch struct. The element ports
|
|
1979
2225
|
// may read the `$as` binding — resolved via the over-element type (prior node / input array).
|
|
1980
2226
|
structs.push(emitMapPortsStructs(c, n, typedNodes, plan));
|
|
@@ -2002,7 +2248,7 @@ function emit(ctx) {
|
|
|
2002
2248
|
const needSync = native.some((c) => concurrencyPlan(c) !== null);
|
|
2003
2249
|
// bc#90: the ONLY std-lib `fmt.Sprintf` site on the covered plane is the batched-map result-count
|
|
2004
2250
|
// mismatch error — so `fmt` is imported iff a covered component has a batched map node.
|
|
2005
|
-
const needFmt = native.some((c) => c.body.some((n) => "map" in n && n.map.batched === true));
|
|
2251
|
+
const needFmt = native.some((c) => c.body.some((n) => ("map" in n && n.map.batched === true) || "fanout" in n));
|
|
2006
2252
|
// The FULLY-covered module (no non-native components) gets a MINIMAL, RUNTIME-FREE header — NO
|
|
2007
2253
|
// bc-runtime import at all (bc#90).
|
|
2008
2254
|
// FAIL CLOSED (bc#86/#89): the --typed-native (native codegen) surface is native-only. If ANY
|
|
@@ -2114,6 +2360,39 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
|
2114
2360
|
if ("cond" in n)
|
|
2115
2361
|
continue; // #108: cond has no handler method (pure Expression) — no scripted impl.
|
|
2116
2362
|
const ref = typedNodes.get(n.id);
|
|
2363
|
+
if ("fanout" in n) {
|
|
2364
|
+
// v3: scripted fanout impl — drain one batched outcome and decode the aligned []Value into the
|
|
2365
|
+
// per-body elem rows (a null array element materializes to the zero elem row = dangling).
|
|
2366
|
+
const f = n.fanout;
|
|
2367
|
+
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
2368
|
+
const elemT = rawElemStructName(c.name, n.id);
|
|
2369
|
+
const batchT = rawRowStructName(c.name, n.id);
|
|
2370
|
+
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2371
|
+
const portsT = `${portsStructName(c.name, n.id)}_batch`;
|
|
2372
|
+
const foBoundT = boundGoType(n, c, typedNodes, plan);
|
|
2373
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${foBoundT}) (${batchT}, bool) {
|
|
2374
|
+
src, ok := s.next(${JSON.stringify(f.component)})
|
|
2375
|
+
if !ok {
|
|
2376
|
+
return ${batchT}{}, false
|
|
2377
|
+
}
|
|
2378
|
+
if src.isErr {
|
|
2379
|
+
return ${batchT}{IsError: true, Err: src.err}, true
|
|
2380
|
+
}
|
|
2381
|
+
arr, _ := src.ok.([]${PKG}.Value)
|
|
2382
|
+
rows := make([]${elemT}, 0, len(arr))
|
|
2383
|
+
for _, ev := range arr {
|
|
2384
|
+
// v3: a null aligned body is a DANGLING id — decode it to the zero elem row (empty dedupeKey
|
|
2385
|
+
// field), so the runner's dangling-drop excludes it (byte-equal to fanoutDedupDrop).
|
|
2386
|
+
if ev == nil {
|
|
2387
|
+
rows = append(rows, ${elemT}{})
|
|
2388
|
+
continue
|
|
2389
|
+
}
|
|
2390
|
+
rows = append(rows, ${elemDecode}(ev))
|
|
2391
|
+
}
|
|
2392
|
+
return ${batchT}{Rows: rows}, true
|
|
2393
|
+
}`);
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2117
2396
|
if ("map" in n) {
|
|
2118
2397
|
const m = n.map;
|
|
2119
2398
|
const batched = m.batched === true;
|