behavior-contracts 0.7.2 → 0.8.0
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/README.md +0 -1
- package/dist/authoring.d.ts +215 -3
- package/dist/authoring.d.ts.map +1 -1
- package/dist/authoring.js +338 -8
- package/dist/authoring.js.map +1 -1
- package/dist/behavior.d.ts +101 -3
- package/dist/behavior.d.ts.map +1 -1
- package/dist/behavior.js +126 -4
- package/dist/behavior.js.map +1 -1
- package/dist/bind.d.ts +64 -0
- package/dist/bind.d.ts.map +1 -0
- package/dist/bind.js +131 -0
- package/dist/bind.js.map +1 -0
- package/dist/builder.d.ts +30 -88
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +114 -59
- package/dist/builder.js.map +1 -1
- package/dist/expr-operator-types.d.ts +89 -0
- package/dist/expr-operator-types.d.ts.map +1 -0
- package/dist/expr-operator-types.js +134 -0
- package/dist/expr-operator-types.js.map +1 -0
- package/dist/generator/async-plan.d.ts.map +1 -1
- package/dist/generator/async-plan.js +4 -0
- package/dist/generator/async-plan.js.map +1 -1
- package/dist/generator/core.d.ts +15 -0
- package/dist/generator/core.d.ts.map +1 -1
- package/dist/generator/core.js +2 -0
- package/dist/generator/core.js.map +1 -1
- package/dist/generator/emit-php.d.ts.map +1 -1
- package/dist/generator/emit-php.js +1 -0
- package/dist/generator/emit-php.js.map +1 -1
- package/dist/generator/emit-python.d.ts.map +1 -1
- package/dist/generator/emit-python.js +1 -0
- package/dist/generator/emit-python.js.map +1 -1
- package/dist/generator/emit-straightline-typed-typescript.d.ts.map +1 -1
- package/dist/generator/emit-straightline-typed-typescript.js +1 -0
- package/dist/generator/emit-straightline-typed-typescript.js.map +1 -1
- package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-go.js +364 -14
- 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 +347 -21
- package/dist/generator/emit-typed-native-rust.js.map +1 -1
- package/dist/generator/emit-typescript.d.ts.map +1 -1
- package/dist/generator/emit-typescript.js +24 -8
- package/dist/generator/emit-typescript.js.map +1 -1
- package/dist/generator/index.d.ts +1 -1
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +7 -5
- package/dist/generator/index.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 +42 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -7
- package/dist/index.js.map +1 -1
- package/dist/provenance.d.ts +102 -0
- package/dist/provenance.d.ts.map +1 -0
- package/dist/provenance.js +128 -0
- package/dist/provenance.js.map +1 -0
- package/dist/type-gate.d.ts +63 -0
- package/dist/type-gate.d.ts.map +1 -0
- package/dist/type-gate.js +295 -0
- package/dist/type-gate.js.map +1 -0
- package/package.json +4 -3
|
@@ -178,13 +178,17 @@ function staticStringArrElems(node) {
|
|
|
178
178
|
* Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no `Value`
|
|
179
179
|
* fallback on the native module). `where` names the emission site for the error.
|
|
180
180
|
*/
|
|
181
|
-
function portFieldRustType(node, inputPorts, where = "port", asBinding, plan) {
|
|
181
|
+
function portFieldRustType(node, inputPorts, where = "port", asBinding, plan, typedNodes) {
|
|
182
182
|
if (staticStringArrElems(node) !== null)
|
|
183
183
|
return "Vec<&'static str>";
|
|
184
184
|
// #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → Vec<ElemT>.
|
|
185
185
|
const arrElem = portArrayElemRef(node, inputPorts, plan);
|
|
186
186
|
if (arrElem !== null)
|
|
187
187
|
return `Vec<${renderTypeRef(arrElem)}>`;
|
|
188
|
+
// #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) → Vec<ElemT>.
|
|
189
|
+
const priorArr = priorNodeArrElemInfo(node, typedNodes, plan);
|
|
190
|
+
if (priorArr !== null)
|
|
191
|
+
return `Vec<${renderTypeRef(priorArr.elemRef)}>`;
|
|
188
192
|
const num = numLiteralRustExpr(node);
|
|
189
193
|
if (num !== null)
|
|
190
194
|
return typeof node === "number" && Number.isInteger(node) ? "i64" : "f64";
|
|
@@ -201,13 +205,13 @@ function portFieldRustType(node, inputPorts, where = "port", asBinding, plan) {
|
|
|
201
205
|
* reads `ports.f_<field>` DIRECTLY. The struct derives Clone (a bc#87 parallel stage clones it into the
|
|
202
206
|
* per-worker dispatch). Every field is a NATIVE Rust type (bc#90 / runtime-free) — NO boxed `Value`.
|
|
203
207
|
*/
|
|
204
|
-
function emitPortsStruct(comp, node, asBinding, plan) {
|
|
208
|
+
function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
|
|
205
209
|
const name = portsStructName(comp.name, node.id);
|
|
206
210
|
const portNames = Object.keys(node.ports);
|
|
207
211
|
if (portNames.length === 0) {
|
|
208
212
|
return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
|
|
209
213
|
}
|
|
210
|
-
const types = portNames.map((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding, plan));
|
|
214
|
+
const types = portNames.map((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding, plan, typedNodes));
|
|
211
215
|
const fields = portNames
|
|
212
216
|
.map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
|
|
213
217
|
.join("\n");
|
|
@@ -242,6 +246,22 @@ pub struct ${batch} {
|
|
|
242
246
|
pub items: Vec<${name}>,
|
|
243
247
|
}`;
|
|
244
248
|
}
|
|
249
|
+
/** emitFanoutPortsStructs (v3, rust twin) — per-id element ports struct + batch struct for a fanout node. */
|
|
250
|
+
function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
|
|
251
|
+
const f = node.fanout;
|
|
252
|
+
const name = portsStructName(comp.name, node.id);
|
|
253
|
+
const batch = `${name}Batch`;
|
|
254
|
+
const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
|
|
255
|
+
const asBinding = { name: f.as, ref: elemRef, plan };
|
|
256
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan);
|
|
257
|
+
return `${elemStruct}
|
|
258
|
+
|
|
259
|
+
// ${batch} — CONCRETE batched ports for fanout '${node.id}': the Vec of per-id CONCRETE ports structs.
|
|
260
|
+
#[derive(Clone)]
|
|
261
|
+
pub struct ${batch} {
|
|
262
|
+
pub items: Vec<${name}>,
|
|
263
|
+
}`;
|
|
264
|
+
}
|
|
245
265
|
// ── native expression compiler backend (#108) — cond `if` / map `when` / cond branches (Rust) ────────
|
|
246
266
|
//
|
|
247
267
|
// The runtime-free native expression compiler (native-expr.ts) mirrors run_behavior's cond/guard with
|
|
@@ -440,6 +460,13 @@ function emitRawRowStructs(comp, typedNodes, plan) {
|
|
|
440
460
|
if ("cond" in n)
|
|
441
461
|
continue; // #108: a cond node has no handler-result row (pure Expression).
|
|
442
462
|
const ref = typedNodes.get(n.id);
|
|
463
|
+
if ("fanout" in n) {
|
|
464
|
+
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
465
|
+
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
466
|
+
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
|
|
467
|
+
`#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub is_error: bool,\n pub err: String,\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
443
470
|
if ("map" in n) {
|
|
444
471
|
const arrRef = ref;
|
|
445
472
|
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
@@ -541,6 +568,13 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
541
568
|
const method = nodeMethodName(n.id);
|
|
542
569
|
const fnKw = fnKwFor(n.id);
|
|
543
570
|
const bt = boundRustType(n, typedNodes, plan);
|
|
571
|
+
if ("fanout" in n) {
|
|
572
|
+
// fanout (v3): one batched dispatch of the deduped id list — batch ports IN, aligned batch row OUT.
|
|
573
|
+
const portsT = `${portsStructName(comp.name, n.id)}Batch`;
|
|
574
|
+
const retT = rawRowStructName(comp.name, n.id);
|
|
575
|
+
lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Option<${retT}>;`);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
544
578
|
if ("map" in n) {
|
|
545
579
|
const m = n.map;
|
|
546
580
|
const batched = m.batched === true;
|
|
@@ -651,6 +685,44 @@ function mapOverElemInfo(comp, node, typedNodes, plan) {
|
|
|
651
685
|
function mapOverElemType(comp, node, typedNodes, plan) {
|
|
652
686
|
return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
|
|
653
687
|
}
|
|
688
|
+
/**
|
|
689
|
+
* priorNodeArrElemInfo (#129, rust twin) — resolve a leaf port that is a `ref` into a PRIOR BODY NODE
|
|
690
|
+
* whose annotated `outType` is (or reaches, through a field path) a native array, to its element TypeRef
|
|
691
|
+
* + the OWNED Rust Vec expression that yields it. GENERALIZES the `map…over` prior-node-arr dataflow
|
|
692
|
+
* (mapOverElemInfo's first branch) to ANY leaf input port: a fold/dedup leaf whose `items` port is
|
|
693
|
+
* `{ref:["<priorNode>"]}` (or `.items` of a prior connection node) now lowers to `Vec<ElemT>` cloned
|
|
694
|
+
* out of the prior node's typed cell — ZERO boxed Value on the read hot path.
|
|
695
|
+
*
|
|
696
|
+
* The element type comes STRICTLY from the prior node's EXPLICIT `outType` annotation walked through
|
|
697
|
+
* typedFieldAccess (BC does NOT infer — consumer-interface C3 / [[graphddb-typed-native-static-link]]):
|
|
698
|
+
* if the head is not a prior body node, or its outType (through the field path) is not an `arr`, this
|
|
699
|
+
* returns null and the caller keeps its existing fail-closed behaviour (never a silent box). A
|
|
700
|
+
* single-segment INPUT-array port (elemType) is a DIFFERENT covered shape (portArrayElemRef) — handled
|
|
701
|
+
* ahead of this. Requires the typedNodes map (prior-node outType TypeRefs) and a TypePlan.
|
|
702
|
+
*/
|
|
703
|
+
function priorNodeArrElemInfo(node, typedNodes, plan) {
|
|
704
|
+
if (typedNodes === undefined || plan === undefined)
|
|
705
|
+
return null;
|
|
706
|
+
if (staticArrElems(node) !== null)
|
|
707
|
+
return null; // a static array literal — not a prior-node ref.
|
|
708
|
+
const e = classifyExpr(node);
|
|
709
|
+
if (e.kind !== "ref" || e.opt || e.path.length < 1)
|
|
710
|
+
return null;
|
|
711
|
+
if (!typedNodes.has(e.path[0]))
|
|
712
|
+
return null; // head is not a prior body node → not this shape.
|
|
713
|
+
const baseRef = typedNodes.get(e.path[0]);
|
|
714
|
+
let acc;
|
|
715
|
+
try {
|
|
716
|
+
acc = typedFieldAccess(`${typedCell(e.path[0])}.borrow()`, baseRef, e.path.slice(1), plan);
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
return null; // the field path does not resolve on the prior node's outType → fail-closed at caller.
|
|
720
|
+
}
|
|
721
|
+
if (acc.ref.kind !== "arr")
|
|
722
|
+
return null; // the prior-node result (through the path) is not an array.
|
|
723
|
+
// OWNED: clone the borrowed slice out of the RefCell so the ports struct owns its Vec<ElemT>.
|
|
724
|
+
return { elemRef: acc.ref.elem, overExpr: `${acc.expr}.clone()` };
|
|
725
|
+
}
|
|
654
726
|
// ── eligibility ─────────────────────────────────────────────────────────────────────
|
|
655
727
|
function reconstructR(e) {
|
|
656
728
|
switch (e.kind) {
|
|
@@ -773,6 +845,63 @@ function coveredMapNode(n, bodyIds, comp) {
|
|
|
773
845
|
return false;
|
|
774
846
|
return true;
|
|
775
847
|
}
|
|
848
|
+
/** coveredFanoutNode (v3, rust twin) — a first-class fanout node is covered when its over is a covered
|
|
849
|
+
* ref (prior-node arr / input array with elemType) and its element ports are static. See the go twin. */
|
|
850
|
+
function coveredFanoutNode(n, bodyIds, comp) {
|
|
851
|
+
if (n === null || typeof n !== "object" || !("fanout" in n))
|
|
852
|
+
return false;
|
|
853
|
+
const f = n.fanout;
|
|
854
|
+
if (f.policy !== undefined && f.policy !== "fail")
|
|
855
|
+
return false;
|
|
856
|
+
if (f.relationKind !== "connection")
|
|
857
|
+
return false;
|
|
858
|
+
const overE = classifyExpr(f.over);
|
|
859
|
+
if (overE.kind !== "ref")
|
|
860
|
+
return false;
|
|
861
|
+
const head = overE.path[0];
|
|
862
|
+
if (!bodyIds.has(head)) {
|
|
863
|
+
if (overE.path.length !== 1)
|
|
864
|
+
return false;
|
|
865
|
+
const schema = comp.inputPorts?.[head];
|
|
866
|
+
if (schema === undefined || schema.elemType === undefined)
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
const ports = f.ports;
|
|
870
|
+
for (const p of Object.keys(ports))
|
|
871
|
+
if (!portIsStatic(ports[p]))
|
|
872
|
+
return false;
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
/** fanoutItemsElemRef (v3, rust twin) — the connection items element TypeRef for a covered fanout node. */
|
|
876
|
+
function fanoutItemsElemRef(node, connRef, plan) {
|
|
877
|
+
if (connRef.kind !== "named")
|
|
878
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust fanout (v3): node '${node.id}' outType must be a connection struct {items,cursor} (got ${JSON.stringify(connRef.kind)}).`);
|
|
879
|
+
const decl = findDecl(plan, connRef.name);
|
|
880
|
+
const itemsField = decl.fields.find((fd) => fd.name === "items");
|
|
881
|
+
if (!itemsField || itemsField.type.kind !== "arr")
|
|
882
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust fanout (v3): node '${node.id}' connection struct '${connRef.name}' has no 'items' array field.`);
|
|
883
|
+
return itemsField.type.elem;
|
|
884
|
+
}
|
|
885
|
+
/** fanoutOverElemInfo (v3, rust twin) — the over id-list element TypeRef + the OWNED over-Vec expr. */
|
|
886
|
+
function fanoutOverElemInfo(comp, node, typedNodes, plan) {
|
|
887
|
+
const f = node.fanout;
|
|
888
|
+
const e = classifyExpr(f.over);
|
|
889
|
+
if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
|
|
890
|
+
const baseRef = typedNodes.get(e.path[0]);
|
|
891
|
+
const { ref } = typedFieldAccess(`${typedCell(e.path[0])}.borrow()`, baseRef, e.path.slice(1), plan);
|
|
892
|
+
if (ref.kind === "arr") {
|
|
893
|
+
const fieldPath = e.path.slice(1).map((p) => `.${rustFieldName(p)}`).join("");
|
|
894
|
+
return { elemRef: ref.elem, overExpr: `${typedCell(e.path[0])}.borrow()${fieldPath}.clone()` };
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (e.kind === "ref" && e.path.length === 1) {
|
|
898
|
+
const schema = comp.inputPorts?.[e.path[0]];
|
|
899
|
+
const et = schema?.elemType;
|
|
900
|
+
if (et !== undefined)
|
|
901
|
+
return { elemRef: deriveTypeRef(et, plan), overExpr: `in_.${rustFieldName(e.path[0])}.clone()` };
|
|
902
|
+
}
|
|
903
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust 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.`);
|
|
904
|
+
}
|
|
776
905
|
/** A covered read SHAPE with static ports + native-lowerable output (+ covered map...into), OR a
|
|
777
906
|
* real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point reads. */
|
|
778
907
|
function isSequentialComponentRefRead(comp) {
|
|
@@ -809,6 +938,11 @@ function coverageRejectReason(comp) {
|
|
|
809
938
|
return `node '${n.id}' cond has no outType (required to de-box the branch join)`;
|
|
810
939
|
continue;
|
|
811
940
|
}
|
|
941
|
+
if ("fanout" in n) {
|
|
942
|
+
if (!coveredFanoutNode(n, bodyIds, comp))
|
|
943
|
+
return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
812
946
|
if ("map" in n) {
|
|
813
947
|
if (!coveredMapNode(n, bodyIds, comp))
|
|
814
948
|
return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
|
|
@@ -977,7 +1111,7 @@ function emitNativeScalar(node, want, resolveRef) {
|
|
|
977
1111
|
* a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
|
|
978
1112
|
* ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
|
|
979
1113
|
*/
|
|
980
|
-
function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan) {
|
|
1114
|
+
function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan, typedNodes) {
|
|
981
1115
|
const field = portFieldName(pn);
|
|
982
1116
|
// #110: a componentRef port bound to a runtime array input port (with elemType) → the native Vec<ElemT>,
|
|
983
1117
|
// cloned out of the concrete input struct (`in_.<field>.clone()`) — ZERO boxed Value on the read hot path.
|
|
@@ -987,6 +1121,12 @@ function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan) {
|
|
|
987
1121
|
return `${field}: in_.${rustFieldName(e.path[0])}.clone()`;
|
|
988
1122
|
}
|
|
989
1123
|
}
|
|
1124
|
+
// #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) →
|
|
1125
|
+
// the native Vec<ElemT> cloned off the prior node's typed cell (`t_<node>.borrow().field….clone()`) —
|
|
1126
|
+
// ZERO boxed Value on the read hot path (a fold/dedup leaf whose `items` = a prior node's array result).
|
|
1127
|
+
const priorArr = priorNodeArrElemInfo(expr, typedNodes, plan);
|
|
1128
|
+
if (priorArr !== null)
|
|
1129
|
+
return `${field}: ${priorArr.overExpr}`;
|
|
990
1130
|
// change #2: a static string array (projection) → a native `vec![...]` of &'static str.
|
|
991
1131
|
const strArr = staticStringArrElems(expr);
|
|
992
1132
|
if (strArr !== null) {
|
|
@@ -1112,6 +1252,14 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1112
1252
|
};
|
|
1113
1253
|
return [...order.keys()].some((k) => {
|
|
1114
1254
|
const nd = comp.body[order[k]];
|
|
1255
|
+
if ("fanout" in nd) {
|
|
1256
|
+
const fo = nd.fanout;
|
|
1257
|
+
const bound = new Set([fo.as]);
|
|
1258
|
+
// the fanout's OVER is evaluated in the outer scope (may read input); ports in element scope.
|
|
1259
|
+
if (walk(fo.over, k, new Set()))
|
|
1260
|
+
return true;
|
|
1261
|
+
return Object.values(fo.ports).some((pn) => walk(pn, k, bound));
|
|
1262
|
+
}
|
|
1115
1263
|
if ("map" in nd) {
|
|
1116
1264
|
const m = nd.map;
|
|
1117
1265
|
const bound = new Set([m.as]);
|
|
@@ -1152,7 +1300,7 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1152
1300
|
// default emits `pub fn` exactly as before (byte-identical).
|
|
1153
1301
|
const fnKw = isAsync ? "pub async fn" : "pub fn";
|
|
1154
1302
|
// #108: a component whose body is ALL cond nodes dispatches NO handler — bind `_handlers` (clippy-clean).
|
|
1155
|
-
const dispatchesHandler = comp.body.some((n) => "map" in n || "component" in n);
|
|
1303
|
+
const dispatchesHandler = comp.body.some((n) => "map" in n || "fanout" in n || "component" in n);
|
|
1156
1304
|
lines.push(`${fnKw} run_native_raw_struct_${fn}${bound}(`);
|
|
1157
1305
|
lines.push(` ${dispatchesHandler ? "handlers" : "_handlers"}: &H,`);
|
|
1158
1306
|
lines.push(` ${inParam}: ${inStructName(comp.name)},`);
|
|
@@ -1171,6 +1319,10 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1171
1319
|
k += stage.length - 1;
|
|
1172
1320
|
continue;
|
|
1173
1321
|
}
|
|
1322
|
+
if ("fanout" in comp.body[i]) {
|
|
1323
|
+
lines.push(emitFanoutArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, ap.nodeIsAsync(comp.body[i].id)));
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1174
1326
|
if ("map" in comp.body[i]) {
|
|
1175
1327
|
lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, ap.nodeIsAsync(comp.body[i].id)));
|
|
1176
1328
|
continue;
|
|
@@ -1219,8 +1371,8 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1219
1371
|
const inits = [];
|
|
1220
1372
|
for (const pn of portNames) {
|
|
1221
1373
|
const expr = node.ports[pn];
|
|
1222
|
-
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan);
|
|
1223
|
-
inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan));
|
|
1374
|
+
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
|
|
1375
|
+
inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan, typedNodes));
|
|
1224
1376
|
}
|
|
1225
1377
|
const boundArg = node.bindField !== undefined ? `bound_${s}` : "None";
|
|
1226
1378
|
// bc#97: derived per-node await — this point-read's future is awaited HERE (its result-consumption
|
|
@@ -1325,8 +1477,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1325
1477
|
const inits = [];
|
|
1326
1478
|
for (const pn of Object.keys(node.ports)) {
|
|
1327
1479
|
const expr = node.ports[pn];
|
|
1328
|
-
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan);
|
|
1329
|
-
inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan));
|
|
1480
|
+
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
|
|
1481
|
+
inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan, typedNodes));
|
|
1330
1482
|
}
|
|
1331
1483
|
if (gated) {
|
|
1332
1484
|
lines.push(`${ind}ports_${s} = Some(${structName} { ${inits.join(", ")} });`);
|
|
@@ -1601,6 +1753,135 @@ isAsync = false) {
|
|
|
1601
1753
|
lines.push(c);
|
|
1602
1754
|
return lines.join("\n");
|
|
1603
1755
|
}
|
|
1756
|
+
/**
|
|
1757
|
+
* emitFanoutArm (v3, rust twin) — the struct-native exec of a covered FANOUT node: build the per-id
|
|
1758
|
+
* ports (all over ids), dispatch the batched handler ONCE (aligned per-body rows), then apply THE SAME
|
|
1759
|
+
* dedupe/drop as run_behavior's fanoutDedupDrop — first-seen dedupe by the elem's dedupeKey field, drop
|
|
1760
|
+
* dangling (empty dedupeKey ≡ the interpreter's null/absent-key body), implicitSource strip (STRUCTURAL:
|
|
1761
|
+
* the elem type already omits it), wrap into the connection struct { items, cursor: None }.
|
|
1762
|
+
*/
|
|
1763
|
+
function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync = false) {
|
|
1764
|
+
const f = node.fanout;
|
|
1765
|
+
const awaitSfx = isAsync ? ".await" : "";
|
|
1766
|
+
const s = sanitize(node.id);
|
|
1767
|
+
const structName = portsStructName(comp.name, node.id);
|
|
1768
|
+
const batch = `${structName}Batch`;
|
|
1769
|
+
const connRef = typedNodes.get(node.id);
|
|
1770
|
+
const elemRef = fanoutItemsElemRef(node, connRef, plan);
|
|
1771
|
+
const elemName = renderTypeRef(elemRef);
|
|
1772
|
+
const { elemRef: overElemRef, overExpr } = fanoutOverElemInfo(comp, node, typedNodes, plan);
|
|
1773
|
+
const asName = f.as;
|
|
1774
|
+
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1775
|
+
const portNames = Object.keys(f.ports);
|
|
1776
|
+
if (elemRef.kind !== "named")
|
|
1777
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust fanout (v3): node '${node.id}' connection items element must be a named struct (got ${JSON.stringify(elemRef.kind)}).`);
|
|
1778
|
+
const elemDecl = findDecl(plan, elemRef.name);
|
|
1779
|
+
const dkField = elemDecl.fields.find((fd) => fd.name === f.dedupeKey);
|
|
1780
|
+
if (!dkField || dkField.type.kind !== "scalar" || dkField.type.scalar !== "string")
|
|
1781
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust fanout (v3): node '${node.id}' dedupeKey '${f.dedupeKey}' must be a string field of the connection items element type '${elemRef.name}'.`);
|
|
1782
|
+
const dkRust = rustFieldName(f.dedupeKey);
|
|
1783
|
+
const resolveRef = (head, restPath, opt) => {
|
|
1784
|
+
if (opt)
|
|
1785
|
+
return null;
|
|
1786
|
+
let baseExpr;
|
|
1787
|
+
let baseRef;
|
|
1788
|
+
if (head === asName) {
|
|
1789
|
+
baseExpr = `oel_${s}`;
|
|
1790
|
+
baseRef = overElemRef;
|
|
1791
|
+
}
|
|
1792
|
+
else if (priorHere(head)) {
|
|
1793
|
+
baseExpr = `${typedCell(head)}.borrow()`;
|
|
1794
|
+
baseRef = typedNodes.get(head);
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
if (restPath.length !== 0)
|
|
1798
|
+
return null;
|
|
1799
|
+
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1800
|
+
if (scalar === undefined)
|
|
1801
|
+
return null;
|
|
1802
|
+
const field = `in_.${rustFieldName(head)}`;
|
|
1803
|
+
return { expr: scalar === "String" ? `${field}.clone()` : field, scalar };
|
|
1804
|
+
}
|
|
1805
|
+
let acc;
|
|
1806
|
+
let expr;
|
|
1807
|
+
try {
|
|
1808
|
+
const r = typedFieldAccess(baseExpr, baseRef, restPath, plan);
|
|
1809
|
+
expr = r.expr;
|
|
1810
|
+
acc = r.ref;
|
|
1811
|
+
}
|
|
1812
|
+
catch {
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1816
|
+
return null;
|
|
1817
|
+
const isBareRef = restPath.length === 0;
|
|
1818
|
+
let owned;
|
|
1819
|
+
if (acc.scalar === "string")
|
|
1820
|
+
owned = isBareRef ? `(*${expr}).clone()` : `${expr}.clone()`;
|
|
1821
|
+
else
|
|
1822
|
+
owned = isBareRef ? `*${expr}` : expr;
|
|
1823
|
+
return { expr: owned, scalar: rustScalar(acc.scalar) };
|
|
1824
|
+
};
|
|
1825
|
+
const buildPorts = (il) => {
|
|
1826
|
+
const inits = [];
|
|
1827
|
+
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1828
|
+
for (const pn of portNames) {
|
|
1829
|
+
const ty = portFieldRustType(f.ports[pn], comp.inputPorts, `fanout port '${pn}' of node '${node.id}'`, asBinding, plan);
|
|
1830
|
+
inits.push(emitPortInit(pn, f.ports[pn], ty, resolveRef, comp.inputPorts, plan));
|
|
1831
|
+
}
|
|
1832
|
+
return [`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`];
|
|
1833
|
+
};
|
|
1834
|
+
const lines = [];
|
|
1835
|
+
const closers = [];
|
|
1836
|
+
lines.push(` // ── 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 : ""}) ──`);
|
|
1837
|
+
let indent = " ";
|
|
1838
|
+
if (f.parent !== undefined && priorNodeAt(f.parent, atPos)) {
|
|
1839
|
+
lines.push(`${indent}if produced_${sanitize(f.parent)}.get() {`);
|
|
1840
|
+
closers.unshift(`${indent}}`);
|
|
1841
|
+
indent += " ";
|
|
1842
|
+
}
|
|
1843
|
+
lines.push(`${indent}let over_${s} = ${overExpr};`);
|
|
1844
|
+
lines.push(`${indent}let mut built_${s}: Vec<${elemName}> = Vec::new();`);
|
|
1845
|
+
lines.push(`${indent}let mut pi_${s}: Vec<${structName}> = Vec::with_capacity(over_${s}.len());`);
|
|
1846
|
+
lines.push(`${indent}for oel_${s} in over_${s}.iter() {`);
|
|
1847
|
+
for (const l of buildPorts(indent + " "))
|
|
1848
|
+
lines.push(l);
|
|
1849
|
+
lines.push(`${indent} pi_${s}.push(ep_${s});`);
|
|
1850
|
+
lines.push(`${indent}}`);
|
|
1851
|
+
lines.push(`${indent}if !pi_${s}.is_empty() {`);
|
|
1852
|
+
lines.push(`${indent} let want_${s} = pi_${s}.len();`);
|
|
1853
|
+
lines.push(`${indent} let bp_${s} = ${batch} { items: pi_${s} };`);
|
|
1854
|
+
lines.push(`${indent} let row_${s} = match handlers.${nodeMethodName(node.id)}(&bp_${s}, None)${awaitSfx} {`);
|
|
1855
|
+
lines.push(`${indent} Some(r) => r,`);
|
|
1856
|
+
lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(f.component)})),`);
|
|
1857
|
+
lines.push(`${indent} };`);
|
|
1858
|
+
lines.push(`${indent} if row_${s}.is_error {`);
|
|
1859
|
+
lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
|
|
1860
|
+
lines.push(`${indent} }`);
|
|
1861
|
+
lines.push(`${indent} if row_${s}.rows.len() != want_${s} {`);
|
|
1862
|
+
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})));`);
|
|
1863
|
+
lines.push(`${indent} }`);
|
|
1864
|
+
// ── THE ONE dedupe/drop (mirrors fanoutDedupDrop verbatim): first-seen dedupe by dedupeKey field;
|
|
1865
|
+
// drop dangling (empty dedupeKey ≡ interpreter's null/absent-key body); implicitSource strip is
|
|
1866
|
+
// structural (the elem type already omits it). cursor is always None (connection wrap).
|
|
1867
|
+
lines.push(`${indent} let mut seen_${s}: std::collections::HashSet<String> = std::collections::HashSet::new();`);
|
|
1868
|
+
lines.push(`${indent} for er_${s} in row_${s}.rows.into_iter() {`);
|
|
1869
|
+
lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemRef, plan)}`);
|
|
1870
|
+
lines.push(`${indent} let key_${s} = el_${s}.${dkRust}.clone();`);
|
|
1871
|
+
if (f.drop === "dangling") {
|
|
1872
|
+
lines.push(`${indent} if key_${s}.is_empty() { continue; }`);
|
|
1873
|
+
}
|
|
1874
|
+
lines.push(`${indent} if seen_${s}.contains(&key_${s}) { continue; }`);
|
|
1875
|
+
lines.push(`${indent} seen_${s}.insert(key_${s});`);
|
|
1876
|
+
lines.push(`${indent} built_${s}.push(el_${s});`);
|
|
1877
|
+
lines.push(`${indent} }`);
|
|
1878
|
+
lines.push(`${indent}}`);
|
|
1879
|
+
lines.push(`${indent}*${typedCell(node.id)}.borrow_mut() = ${connRef.name} { ${rustFieldName("items")}: built_${s}, ..Default::default() };`);
|
|
1880
|
+
lines.push(`${indent}produced_${sanitize(node.id)}.set(true);`);
|
|
1881
|
+
for (const c of closers)
|
|
1882
|
+
lines.push(c);
|
|
1883
|
+
return lines.join("\n");
|
|
1884
|
+
}
|
|
1604
1885
|
/** #108: resolve an INPUT port head for a native-expr ref (rust). Scalar → `in_.<field>` (owned: String
|
|
1605
1886
|
* cloned, Copy as-is); array WITH elemType → `in_.<field>.clone()` of Vec<ElemT> (for `len`); else null. */
|
|
1606
1887
|
function rustInputHeadRef(head, comp, plan) {
|
|
@@ -1800,13 +2081,15 @@ ${emitStructSurface([])}
|
|
|
1800
2081
|
for (const n of c.body)
|
|
1801
2082
|
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1802
2083
|
for (const n of c.body) {
|
|
1803
|
-
if ("
|
|
2084
|
+
if ("fanout" in n)
|
|
2085
|
+
structs.push(emitFanoutPortsStructs(c, n, typedNodes, plan));
|
|
2086
|
+
else if ("map" in n)
|
|
1804
2087
|
structs.push(emitMapPortsStructs(c, n, typedNodes, plan));
|
|
1805
2088
|
else if ("cond" in n) {
|
|
1806
2089
|
// #108: a cond node has NO ports struct (pure Expression — no handler dispatch).
|
|
1807
2090
|
}
|
|
1808
2091
|
else
|
|
1809
|
-
structs.push(emitPortsStruct(c, n, undefined, plan));
|
|
2092
|
+
structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
|
|
1810
2093
|
}
|
|
1811
2094
|
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
1812
2095
|
handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
|
|
@@ -1904,6 +2187,34 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
1904
2187
|
// bc#90 / runtime-free: the scripted impl's `_bound` param mirrors the trait's typed produced-aware
|
|
1905
2188
|
// Option<K> (NOT a boxed Value) — computed per node so the impl signature matches the trait.
|
|
1906
2189
|
const bt = boundRustType(n, typedNodes, plan);
|
|
2190
|
+
if ("fanout" in n) {
|
|
2191
|
+
// v3: scripted fanout impl — drain one batched outcome, decode the aligned array into per-body
|
|
2192
|
+
// elem rows (a null array element decodes to the zero elem row = dangling → dropped by the arm).
|
|
2193
|
+
const f = n.fanout;
|
|
2194
|
+
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
2195
|
+
const elemT = rawElemStructName(c.name, n.id);
|
|
2196
|
+
const batchT = rawRowStructName(c.name, n.id);
|
|
2197
|
+
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2198
|
+
const portsT = `${portsStructName(c.name, n.id)}Batch`;
|
|
2199
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
|
|
2200
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)})?;
|
|
2201
|
+
if is_err {
|
|
2202
|
+
return Some(${batchT} { is_error: true, err, ..Default::default() });
|
|
2203
|
+
}
|
|
2204
|
+
let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
|
|
2205
|
+
// v3: a null aligned body is a DANGLING id — decode it to the zero elem row (whose dedupeKey
|
|
2206
|
+
// field is empty), so the runner's dangling-drop excludes it (byte-equal to fanoutDedupDrop).
|
|
2207
|
+
let rows = arr
|
|
2208
|
+
.iter()
|
|
2209
|
+
.map(|ev| match ev {
|
|
2210
|
+
Value::Null => ${elemT}::default(),
|
|
2211
|
+
_ => ${elemDecode}(ev),
|
|
2212
|
+
})
|
|
2213
|
+
.collect();
|
|
2214
|
+
Some(${batchT} { rows, ..Default::default() })
|
|
2215
|
+
}`);
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
1907
2218
|
if ("map" in n) {
|
|
1908
2219
|
const m = n.map;
|
|
1909
2220
|
const batched = m.batched === true;
|
|
@@ -1916,7 +2227,7 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
1916
2227
|
const retT = batched ? batchT : elemT;
|
|
1917
2228
|
if (batched) {
|
|
1918
2229
|
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
|
|
1919
|
-
let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
|
|
2230
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)})?;
|
|
1920
2231
|
if is_err {
|
|
1921
2232
|
return Some(${batchT} { is_error: true, err, ..Default::default() });
|
|
1922
2233
|
}
|
|
@@ -1927,7 +2238,7 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
1927
2238
|
}
|
|
1928
2239
|
else {
|
|
1929
2240
|
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${elemT}> {
|
|
1930
|
-
let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
|
|
2241
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)})?;
|
|
1931
2242
|
if is_err {
|
|
1932
2243
|
return Some(${elemT} { is_error: true, err, ..Default::default() });
|
|
1933
2244
|
}
|
|
@@ -1941,11 +2252,22 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
1941
2252
|
const node = n;
|
|
1942
2253
|
const rowT = rawRowStructName(c.name, node.id);
|
|
1943
2254
|
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
1944
|
-
|
|
1945
|
-
|
|
2255
|
+
// #129 (TEST glue): if this node has an `items` port and its outType is a bare arr, support the
|
|
2256
|
+
// reference scriptedHandlers' `echo === "items"` — the scripted handler ECHOES the NATIVE `f_items`
|
|
2257
|
+
// port back as its result. This makes the node→leaf array dataflow test non-vacuous: the leaf's
|
|
2258
|
+
// observed result is exactly the native array that flowed into its ports struct, so a dropped or
|
|
2259
|
+
// boxed native array would diverge from run_behavior. `ports` is bound only on the echo path.
|
|
2260
|
+
const canEcho = ref.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items");
|
|
2261
|
+
const portsParam = canEcho ? "ports" : "_ports";
|
|
2262
|
+
const echoBranch = canEcho
|
|
2263
|
+
? `\n if echo == "items" {\n return Some(${rowT} { val: ${portsParam}.${portFieldName("items")}.clone(), ..Default::default() });\n }`
|
|
2264
|
+
: "";
|
|
2265
|
+
const okBind = canEcho ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
|
|
2266
|
+
methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
|
|
2267
|
+
let ${okBind} = self.next(${rustStrLit(node.component)})?;
|
|
1946
2268
|
if is_err {
|
|
1947
2269
|
return Some(${rowT} { is_error: true, err, ..Default::default() });
|
|
1948
|
-
}
|
|
2270
|
+
}${echoBranch}
|
|
1949
2271
|
let v = ok.unwrap_or(Value::Null);
|
|
1950
2272
|
Some(${decode}(&v))
|
|
1951
2273
|
}`);
|
|
@@ -2000,19 +2322,22 @@ impl ScriptedNativeRaw {
|
|
|
2000
2322
|
|
|
2001
2323
|
// drain one scripted outcome for a component (last entry repeats — mirrors the reference scripted
|
|
2002
2324
|
// handlers); None when the component has no scripted queue (fail-closed). Returns (is_error, err,
|
|
2003
|
-
// ok_value). The bc#87 parallel-stage runner calls node_* from multiple threads, so the drain
|
|
2004
|
-
// Mutex-guarded.
|
|
2005
|
-
|
|
2325
|
+
// ok_value, echo). The bc#87 parallel-stage runner calls node_* from multiple threads, so the drain
|
|
2326
|
+
// is Mutex-guarded. #129: \`echo\` carries the reference scriptedHandlers' \`echo\` directive (e.g.
|
|
2327
|
+
// "items") so a scripted node can ECHO a named input PORT back — used to make the node→leaf array
|
|
2328
|
+
// dataflow test non-vacuous (the leaf returns its \`items\` port; a dropped/boxed array diverges).
|
|
2329
|
+
fn next(&self, component: &str) -> Option<(bool, String, Option<Value>, String)> {
|
|
2006
2330
|
self.seen.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
2007
2331
|
let mut queues = self.queues.lock().unwrap();
|
|
2008
2332
|
let (queue, single) = queues.get_mut(component)?;
|
|
2009
2333
|
let raw = if *single || queue.len() <= 1 { queue[0].clone() } else { queue.remove(0) };
|
|
2334
|
+
let echo = raw.get("echo").and_then(|e| e.as_str()).unwrap_or("").to_string();
|
|
2010
2335
|
if let Some(okv) = raw.get("ok") {
|
|
2011
2336
|
let v = behavior_contracts::decode_value(okv).expect("decode ok");
|
|
2012
|
-
Some((false, String::new(), Some(v)))
|
|
2337
|
+
Some((false, String::new(), Some(v), echo))
|
|
2013
2338
|
} else {
|
|
2014
2339
|
let e = raw.get("error").and_then(|e| e.as_str()).unwrap_or("").to_string();
|
|
2015
|
-
Some((true, e, None))
|
|
2340
|
+
Some((true, e, None, echo))
|
|
2016
2341
|
}
|
|
2017
2342
|
}
|
|
2018
2343
|
}`;
|
|
@@ -2179,6 +2504,7 @@ function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix)
|
|
|
2179
2504
|
}
|
|
2180
2505
|
export const rustTypedNativeEmitter = {
|
|
2181
2506
|
language: "rust-typed-native",
|
|
2507
|
+
classification: "codegen", // native: 型付きフラットコード生成・IR/dict 非在(#128/A6)
|
|
2182
2508
|
fileExtension: ".rs",
|
|
2183
2509
|
defaultRuntimeImport: "behavior_contracts",
|
|
2184
2510
|
filenameHint: "behaviors_typed_native_generated.rs",
|