behavior-contracts 0.7.3 → 0.8.1
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 +269 -8
- package/dist/authoring.js.map +1 -1
- package/dist/behavior.d.ts +33 -1
- package/dist/behavior.d.ts.map +1 -1
- package/dist/behavior.js +49 -1
- 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 +48 -56
- 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 +163 -26
- 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 +152 -32
- 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/index.d.ts +41 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -6
- 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 +313 -0
- package/dist/type-gate.js.map +1 -0
- package/package.json +4 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GeneratorFailure } from "./core.js";
|
|
2
|
+
import { isControlGate } from "../behavior.js";
|
|
2
3
|
import { rustStraightlineInternals } from "./emit-shared-rust.js";
|
|
3
4
|
import { rustTypedInternals } from "./emit-shared-rust-typed.js";
|
|
4
5
|
import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
|
|
@@ -178,13 +179,17 @@ function staticStringArrElems(node) {
|
|
|
178
179
|
* Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no `Value`
|
|
179
180
|
* fallback on the native module). `where` names the emission site for the error.
|
|
180
181
|
*/
|
|
181
|
-
function portFieldRustType(node, inputPorts, where = "port", asBinding, plan) {
|
|
182
|
+
function portFieldRustType(node, inputPorts, where = "port", asBinding, plan, typedNodes) {
|
|
182
183
|
if (staticStringArrElems(node) !== null)
|
|
183
184
|
return "Vec<&'static str>";
|
|
184
185
|
// #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → Vec<ElemT>.
|
|
185
186
|
const arrElem = portArrayElemRef(node, inputPorts, plan);
|
|
186
187
|
if (arrElem !== null)
|
|
187
188
|
return `Vec<${renderTypeRef(arrElem)}>`;
|
|
189
|
+
// #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) → Vec<ElemT>.
|
|
190
|
+
const priorArr = priorNodeArrElemInfo(node, typedNodes, plan);
|
|
191
|
+
if (priorArr !== null)
|
|
192
|
+
return `Vec<${renderTypeRef(priorArr.elemRef)}>`;
|
|
188
193
|
const num = numLiteralRustExpr(node);
|
|
189
194
|
if (num !== null)
|
|
190
195
|
return typeof node === "number" && Number.isInteger(node) ? "i64" : "f64";
|
|
@@ -201,13 +206,13 @@ function portFieldRustType(node, inputPorts, where = "port", asBinding, plan) {
|
|
|
201
206
|
* reads `ports.f_<field>` DIRECTLY. The struct derives Clone (a bc#87 parallel stage clones it into the
|
|
202
207
|
* per-worker dispatch). Every field is a NATIVE Rust type (bc#90 / runtime-free) — NO boxed `Value`.
|
|
203
208
|
*/
|
|
204
|
-
function emitPortsStruct(comp, node, asBinding, plan) {
|
|
209
|
+
function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
|
|
205
210
|
const name = portsStructName(comp.name, node.id);
|
|
206
211
|
const portNames = Object.keys(node.ports);
|
|
207
212
|
if (portNames.length === 0) {
|
|
208
213
|
return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
|
|
209
214
|
}
|
|
210
|
-
const types = portNames.map((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding, plan));
|
|
215
|
+
const types = portNames.map((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding, plan, typedNodes));
|
|
211
216
|
const fields = portNames
|
|
212
217
|
.map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
|
|
213
218
|
.join("\n");
|
|
@@ -681,6 +686,44 @@ function mapOverElemInfo(comp, node, typedNodes, plan) {
|
|
|
681
686
|
function mapOverElemType(comp, node, typedNodes, plan) {
|
|
682
687
|
return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
|
|
683
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* priorNodeArrElemInfo (#129, rust twin) — resolve a leaf port that is a `ref` into a PRIOR BODY NODE
|
|
691
|
+
* whose annotated `outType` is (or reaches, through a field path) a native array, to its element TypeRef
|
|
692
|
+
* + the OWNED Rust Vec expression that yields it. GENERALIZES the `map…over` prior-node-arr dataflow
|
|
693
|
+
* (mapOverElemInfo's first branch) to ANY leaf input port: a fold/dedup leaf whose `items` port is
|
|
694
|
+
* `{ref:["<priorNode>"]}` (or `.items` of a prior connection node) now lowers to `Vec<ElemT>` cloned
|
|
695
|
+
* out of the prior node's typed cell — ZERO boxed Value on the read hot path.
|
|
696
|
+
*
|
|
697
|
+
* The element type comes STRICTLY from the prior node's EXPLICIT `outType` annotation walked through
|
|
698
|
+
* typedFieldAccess (BC does NOT infer — consumer-interface C3 / [[graphddb-typed-native-static-link]]):
|
|
699
|
+
* if the head is not a prior body node, or its outType (through the field path) is not an `arr`, this
|
|
700
|
+
* returns null and the caller keeps its existing fail-closed behaviour (never a silent box). A
|
|
701
|
+
* single-segment INPUT-array port (elemType) is a DIFFERENT covered shape (portArrayElemRef) — handled
|
|
702
|
+
* ahead of this. Requires the typedNodes map (prior-node outType TypeRefs) and a TypePlan.
|
|
703
|
+
*/
|
|
704
|
+
function priorNodeArrElemInfo(node, typedNodes, plan) {
|
|
705
|
+
if (typedNodes === undefined || plan === undefined)
|
|
706
|
+
return null;
|
|
707
|
+
if (staticArrElems(node) !== null)
|
|
708
|
+
return null; // a static array literal — not a prior-node ref.
|
|
709
|
+
const e = classifyExpr(node);
|
|
710
|
+
if (e.kind !== "ref" || e.opt || e.path.length < 1)
|
|
711
|
+
return null;
|
|
712
|
+
if (!typedNodes.has(e.path[0]))
|
|
713
|
+
return null; // head is not a prior body node → not this shape.
|
|
714
|
+
const baseRef = typedNodes.get(e.path[0]);
|
|
715
|
+
let acc;
|
|
716
|
+
try {
|
|
717
|
+
acc = typedFieldAccess(`${typedCell(e.path[0])}.borrow()`, baseRef, e.path.slice(1), plan);
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
return null; // the field path does not resolve on the prior node's outType → fail-closed at caller.
|
|
721
|
+
}
|
|
722
|
+
if (acc.ref.kind !== "arr")
|
|
723
|
+
return null; // the prior-node result (through the path) is not an array.
|
|
724
|
+
// OWNED: clone the borrowed slice out of the RefCell so the ports struct owns its Vec<ElemT>.
|
|
725
|
+
return { elemRef: acc.ref.elem, overExpr: `${acc.expr}.clone()` };
|
|
726
|
+
}
|
|
684
727
|
// ── eligibility ─────────────────────────────────────────────────────────────────────
|
|
685
728
|
function reconstructR(e) {
|
|
686
729
|
switch (e.kind) {
|
|
@@ -884,14 +927,27 @@ function coverageRejectReason(comp) {
|
|
|
884
927
|
// shape). Kept fail-closed: a non-componentRef member is rejected.
|
|
885
928
|
for (const [i] of ep.parallelStageOf) {
|
|
886
929
|
const n = comp.body[i];
|
|
930
|
+
// #125: a when(...) control-gate cond IS allowed as a parallel-stage member — it is a pure
|
|
931
|
+
// Expression (no dispatch), settled in ascending preflight order like the bindField key gate, so its
|
|
932
|
+
// produced_<gate> flag is决定済み before the stage's gated children dispatch. No handler, no boxed
|
|
933
|
+
// result — it does not disturb the bounded-concurrency error-precedence protocol.
|
|
934
|
+
if (isControlGate(n))
|
|
935
|
+
continue;
|
|
887
936
|
if (!("component" in n))
|
|
888
937
|
return `node '${n.id}' is a non-componentRef member of a parallel stage (not native-covered)`;
|
|
889
938
|
}
|
|
890
939
|
const bodyIds = new Set(comp.body.map((b) => b.id));
|
|
891
940
|
for (const n of comp.body) {
|
|
892
941
|
if ("cond" in n) {
|
|
893
|
-
// #
|
|
894
|
-
//
|
|
942
|
+
// #125: a when(...) control-gate cond (`{if, then:{obj:{ok:true}}, else:null}`) は制御スキップ
|
|
943
|
+
// マーカーで出力データ型を持たない(A3 の typeless design)— outType を要求しない。gate は `if` を
|
|
944
|
+
// native bool へコンパイルし produced_<gate> に写すだけで、分岐内ノードは既存の
|
|
945
|
+
// `parent=gate, bindField:"ok"` skip-gate(run_behavior の null-binding skip)が実行選択する。
|
|
946
|
+
if (isControlGate(n))
|
|
947
|
+
continue;
|
|
948
|
+
// #108: a DATA-JOIN cond node (Φ 合流) is native-covered (its if/then/else compile via the runtime-
|
|
949
|
+
// free NativeExprCompiler; an uncovered expr fails closed at emit). A data-join cond requires outType
|
|
950
|
+
// (合流値を de-box する対象だから — control gate だけがこの要求を免除される)。
|
|
895
951
|
if (n.outType === undefined)
|
|
896
952
|
return `node '${n.id}' cond has no outType (required to de-box the branch join)`;
|
|
897
953
|
continue;
|
|
@@ -911,7 +967,12 @@ function coverageRejectReason(comp) {
|
|
|
911
967
|
const cr = n;
|
|
912
968
|
if (cr.policy !== undefined && cr.policy !== "fail")
|
|
913
969
|
return `node '${n.id}' componentRef policy '${cr.policy}' (produced-aware output not yet native-covered)`;
|
|
914
|
-
|
|
970
|
+
// #125: a when(...) control-gate child (`parent=gate, bindField:"ok"`). Its bindField is the gate's
|
|
971
|
+
// skip signal, NOT a relation data key — so the relationKind constraint below (which governs a
|
|
972
|
+
// relation bindField into a single|connection parent) does not apply. Its skip is driven by the
|
|
973
|
+
// gate's produced_<gate> flag; the child itself can be a plain point read (or a relation read).
|
|
974
|
+
const childOfGate = cr.parent !== undefined && isControlGate(comp.body.find((b) => b.id === cr.parent));
|
|
975
|
+
if (!childOfGate && cr.bindField !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
915
976
|
return `node '${n.id}' relationKind '${cr.relationKind ?? "point"}' not native-covered (bindField only supports single|connection)`;
|
|
916
977
|
if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
917
978
|
return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
|
|
@@ -926,8 +987,10 @@ function coverageRejectReason(comp) {
|
|
|
926
987
|
function isFullyTyped(comp) {
|
|
927
988
|
if (comp.outputType === undefined)
|
|
928
989
|
return false;
|
|
990
|
+
// #125: a control-gate cond is a typeless skip marker (A3) — it never de-boxes a result, so it is
|
|
991
|
+
// exempt from the "every node carries outType" de-box requirement (matches type-gate's exemption).
|
|
929
992
|
for (const n of comp.body)
|
|
930
|
-
if (n.outType === undefined)
|
|
993
|
+
if (!isControlGate(n) && n.outType === undefined)
|
|
931
994
|
return false;
|
|
932
995
|
return true;
|
|
933
996
|
}
|
|
@@ -1069,7 +1132,7 @@ function emitNativeScalar(node, want, resolveRef) {
|
|
|
1069
1132
|
* a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
|
|
1070
1133
|
* ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
|
|
1071
1134
|
*/
|
|
1072
|
-
function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan) {
|
|
1135
|
+
function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan, typedNodes) {
|
|
1073
1136
|
const field = portFieldName(pn);
|
|
1074
1137
|
// #110: a componentRef port bound to a runtime array input port (with elemType) → the native Vec<ElemT>,
|
|
1075
1138
|
// cloned out of the concrete input struct (`in_.<field>.clone()`) — ZERO boxed Value on the read hot path.
|
|
@@ -1079,6 +1142,12 @@ function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan) {
|
|
|
1079
1142
|
return `${field}: in_.${rustFieldName(e.path[0])}.clone()`;
|
|
1080
1143
|
}
|
|
1081
1144
|
}
|
|
1145
|
+
// #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) →
|
|
1146
|
+
// the native Vec<ElemT> cloned off the prior node's typed cell (`t_<node>.borrow().field….clone()`) —
|
|
1147
|
+
// ZERO boxed Value on the read hot path (a fold/dedup leaf whose `items` = a prior node's array result).
|
|
1148
|
+
const priorArr = priorNodeArrElemInfo(expr, typedNodes, plan);
|
|
1149
|
+
if (priorArr !== null)
|
|
1150
|
+
return `${field}: ${priorArr.overExpr}`;
|
|
1082
1151
|
// change #2: a static string array (projection) → a native `vec![...]` of &'static str.
|
|
1083
1152
|
const strArr = staticStringArrElems(expr);
|
|
1084
1153
|
if (strArr !== null) {
|
|
@@ -1165,7 +1234,8 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1165
1234
|
}
|
|
1166
1235
|
const typedNodes = new Map();
|
|
1167
1236
|
for (const n of comp.body)
|
|
1168
|
-
|
|
1237
|
+
if (!isControlGate(n))
|
|
1238
|
+
typedNodes.set(n.id, deriveTypeRef(n.outType, plan)); // #125: control-gate は typeless(typedNodes に載せない)
|
|
1169
1239
|
const idToIndex = new Map();
|
|
1170
1240
|
comp.body.forEach((n, i) => idToIndex.set(n.id, i));
|
|
1171
1241
|
const opsLiteral = comp.body.map((n) => {
|
|
@@ -1258,8 +1328,13 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1258
1328
|
lines.push(` ${inParam}: ${inStructName(comp.name)},`);
|
|
1259
1329
|
lines.push(`) -> Result<${outTy}, BehaviorError> {`);
|
|
1260
1330
|
for (const k of order.keys()) {
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1331
|
+
const nd = comp.body[order[k]];
|
|
1332
|
+
const id = nd.id;
|
|
1333
|
+
// #125: a control-gate cond has no typed data cell — only a produced_<gate> flag (its `if` result),
|
|
1334
|
+
// which its gated children read via the existing `if produced_<parent>.get()` skip-gate.
|
|
1335
|
+
if (!isControlGate(nd)) {
|
|
1336
|
+
lines.push(` let ${typedCell(id)}: RefCell<${renderTypeRef(typedNodes.get(id))}> = RefCell::new(Default::default());`);
|
|
1337
|
+
}
|
|
1263
1338
|
lines.push(` let produced_${sanitize(id)} = std::cell::Cell::new(false);`);
|
|
1264
1339
|
lines.push(` let _ = &produced_${sanitize(id)};`);
|
|
1265
1340
|
}
|
|
@@ -1293,11 +1368,19 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1293
1368
|
lines.push(` // ── op '${node.id}' (${node.component}${node.relationKind ? ", relationKind:" + node.relationKind : ""}${node.parent ? ", parent:" + node.parent : ""}) ──`);
|
|
1294
1369
|
let indent = " ";
|
|
1295
1370
|
const closers = [];
|
|
1371
|
+
const parentIsGate = node.parent !== undefined && isControlGate(comp.body.find((b) => b.id === node.parent));
|
|
1296
1372
|
if (node.parent !== undefined && priorNodeAt(node.parent, k)) {
|
|
1297
1373
|
lines.push(`${indent}if produced_${sanitize(node.parent)}.get() {`);
|
|
1298
1374
|
closers.unshift(`${indent}}`);
|
|
1299
1375
|
indent += " ";
|
|
1300
|
-
if (node.bindField !== undefined) {
|
|
1376
|
+
if (node.bindField !== undefined && parentIsGate) {
|
|
1377
|
+
// #125: a when(...) control-gate parent. gate の bindField:"ok" は run_behavior では成立時 `true`
|
|
1378
|
+
// / 不成立時 `null`(→ null-binding skip)。native では produced_<gate>(= gate の `if`)が既に
|
|
1379
|
+
// その skip を担うので、ここに来た時点で gate は成立。bound は skip の sentinel(データキーでは
|
|
1380
|
+
// ない — 子の ports は実データ親を参照する)として、非 None の `Option<String>` を渡す。
|
|
1381
|
+
lines.push(`${indent}let bound_${s}: ${boundRustType(node, typedNodes, plan)} = Some("ok".to_string());`);
|
|
1382
|
+
}
|
|
1383
|
+
else if (node.bindField !== undefined) {
|
|
1301
1384
|
// bind (change #3 — typed produced-aware bound): read the parent struct field DIRECTLY (typed
|
|
1302
1385
|
// access — no serialize). `bound_<s>` is a typed `Option<K>` — None => UNPRODUCED/skip, Some =>
|
|
1303
1386
|
// produced (an empty string is a VALID produced value, `Some("")`). This is the EXACT `!= Null`
|
|
@@ -1323,8 +1406,8 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1323
1406
|
const inits = [];
|
|
1324
1407
|
for (const pn of portNames) {
|
|
1325
1408
|
const expr = node.ports[pn];
|
|
1326
|
-
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan);
|
|
1327
|
-
inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan));
|
|
1409
|
+
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
|
|
1410
|
+
inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan, typedNodes));
|
|
1328
1411
|
}
|
|
1329
1412
|
const boundArg = node.bindField !== undefined ? `bound_${s}` : "None";
|
|
1330
1413
|
// bc#97: derived per-node await — this point-read's future is awaited HERE (its result-consumption
|
|
@@ -1429,8 +1512,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1429
1512
|
const inits = [];
|
|
1430
1513
|
for (const pn of Object.keys(node.ports)) {
|
|
1431
1514
|
const expr = node.ports[pn];
|
|
1432
|
-
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan);
|
|
1433
|
-
inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan));
|
|
1515
|
+
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
|
|
1516
|
+
inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan, typedNodes));
|
|
1434
1517
|
}
|
|
1435
1518
|
if (gated) {
|
|
1436
1519
|
lines.push(`${ind}ports_${s} = Some(${structName} { ${inits.join(", ")} });`);
|
|
@@ -1862,7 +1945,6 @@ function rustInputHeadRef(head, comp, plan) {
|
|
|
1862
1945
|
function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
|
|
1863
1946
|
const c = node.cond;
|
|
1864
1947
|
const s = sanitize(node.id);
|
|
1865
|
-
const nodeRef = typedNodes.get(node.id);
|
|
1866
1948
|
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1867
1949
|
const resolveHead = (head) => {
|
|
1868
1950
|
if (priorHere(head))
|
|
@@ -1870,8 +1952,29 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
|
|
|
1870
1952
|
return rustInputHeadRef(head, comp, plan);
|
|
1871
1953
|
};
|
|
1872
1954
|
const compiler = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan));
|
|
1873
|
-
const resolveDecl = (name) => findDecl(plan, name).fields;
|
|
1874
1955
|
const cond = compiler.compileBool(c.if);
|
|
1956
|
+
// #125: a when(...) control-gate cond は出力データ型を持たない制御スキップマーカー。typed cell へ何も
|
|
1957
|
+
// materialize せず、`if` を native bool にして produced_<gate> に写すだけ。分岐内ノード
|
|
1958
|
+
// (`parent=gate, bindField:"ok"`)は既存の `if produced_<gate>.get()` skip-gate が実行選択する。
|
|
1959
|
+
if (isControlGate(node)) {
|
|
1960
|
+
const glines = [];
|
|
1961
|
+
glines.push(` // ── control-gate cond '${node.id}'${c.parent ? " (parent:" + c.parent + ")" : ""} — when(...) skip marker (no data type) ──`);
|
|
1962
|
+
let gindent = " ";
|
|
1963
|
+
const gclosers = [];
|
|
1964
|
+
if (c.parent !== undefined && priorNodeAt(c.parent, atPos)) {
|
|
1965
|
+
glines.push(`${gindent}if produced_${sanitize(c.parent)}.get() {`);
|
|
1966
|
+
gclosers.unshift(`${gindent}}`);
|
|
1967
|
+
gindent += " ";
|
|
1968
|
+
}
|
|
1969
|
+
for (const st of cond.stmts)
|
|
1970
|
+
glines.push(`${gindent}${st}`);
|
|
1971
|
+
glines.push(`${gindent}produced_${s}.set(${rustStripOuterParens(cond.expr)});`);
|
|
1972
|
+
for (const cl of gclosers)
|
|
1973
|
+
glines.push(cl);
|
|
1974
|
+
return glines.join("\n");
|
|
1975
|
+
}
|
|
1976
|
+
const nodeRef = typedNodes.get(node.id);
|
|
1977
|
+
const resolveDecl = (name) => findDecl(plan, name).fields;
|
|
1875
1978
|
const thenC = compiler.compileTo(c.then, nodeRef, resolveDecl);
|
|
1876
1979
|
const elseC = compiler.compileTo(c.else, nodeRef, resolveDecl);
|
|
1877
1980
|
const lines = [];
|
|
@@ -2031,7 +2134,8 @@ ${emitStructSurface([])}
|
|
|
2031
2134
|
const typedNodes = new Map();
|
|
2032
2135
|
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
2033
2136
|
for (const n of c.body)
|
|
2034
|
-
|
|
2137
|
+
if (!isControlGate(n))
|
|
2138
|
+
typedNodes.set(n.id, deriveTypeRef(n.outType, plan)); // #125: control-gate は typeless(typedNodes に載せない)
|
|
2035
2139
|
for (const n of c.body) {
|
|
2036
2140
|
if ("fanout" in n)
|
|
2037
2141
|
structs.push(emitFanoutPortsStructs(c, n, typedNodes, plan));
|
|
@@ -2041,7 +2145,7 @@ ${emitStructSurface([])}
|
|
|
2041
2145
|
// #108: a cond node has NO ports struct (pure Expression — no handler dispatch).
|
|
2042
2146
|
}
|
|
2043
2147
|
else
|
|
2044
|
-
structs.push(emitPortsStruct(c, n, undefined, plan));
|
|
2148
|
+
structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
|
|
2045
2149
|
}
|
|
2046
2150
|
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
2047
2151
|
handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
|
|
@@ -2129,7 +2233,8 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2129
2233
|
const fnKwFor = (nodeId) => (ap.nodeIsAsync(nodeId) ? "async fn" : "fn");
|
|
2130
2234
|
const typedNodes = new Map();
|
|
2131
2235
|
for (const n of c.body)
|
|
2132
|
-
|
|
2236
|
+
if (!isControlGate(n))
|
|
2237
|
+
typedNodes.set(n.id, deriveTypeRef(n.outType, plan)); // #125: control-gate は typeless(typedNodes に載せない)
|
|
2133
2238
|
const methods = [];
|
|
2134
2239
|
for (const n of c.body) {
|
|
2135
2240
|
if ("cond" in n)
|
|
@@ -2149,7 +2254,7 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2149
2254
|
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2150
2255
|
const portsT = `${portsStructName(c.name, n.id)}Batch`;
|
|
2151
2256
|
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
|
|
2152
|
-
let (is_err, err, ok) = self.next(${rustStrLit(f.component)})?;
|
|
2257
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)})?;
|
|
2153
2258
|
if is_err {
|
|
2154
2259
|
return Some(${batchT} { is_error: true, err, ..Default::default() });
|
|
2155
2260
|
}
|
|
@@ -2179,7 +2284,7 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2179
2284
|
const retT = batched ? batchT : elemT;
|
|
2180
2285
|
if (batched) {
|
|
2181
2286
|
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
|
|
2182
|
-
let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
|
|
2287
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)})?;
|
|
2183
2288
|
if is_err {
|
|
2184
2289
|
return Some(${batchT} { is_error: true, err, ..Default::default() });
|
|
2185
2290
|
}
|
|
@@ -2190,7 +2295,7 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2190
2295
|
}
|
|
2191
2296
|
else {
|
|
2192
2297
|
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${elemT}> {
|
|
2193
|
-
let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
|
|
2298
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)})?;
|
|
2194
2299
|
if is_err {
|
|
2195
2300
|
return Some(${elemT} { is_error: true, err, ..Default::default() });
|
|
2196
2301
|
}
|
|
@@ -2204,11 +2309,22 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2204
2309
|
const node = n;
|
|
2205
2310
|
const rowT = rawRowStructName(c.name, node.id);
|
|
2206
2311
|
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
2207
|
-
|
|
2208
|
-
|
|
2312
|
+
// #129 (TEST glue): if this node has an `items` port and its outType is a bare arr, support the
|
|
2313
|
+
// reference scriptedHandlers' `echo === "items"` — the scripted handler ECHOES the NATIVE `f_items`
|
|
2314
|
+
// port back as its result. This makes the node→leaf array dataflow test non-vacuous: the leaf's
|
|
2315
|
+
// observed result is exactly the native array that flowed into its ports struct, so a dropped or
|
|
2316
|
+
// boxed native array would diverge from run_behavior. `ports` is bound only on the echo path.
|
|
2317
|
+
const canEcho = ref.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items");
|
|
2318
|
+
const portsParam = canEcho ? "ports" : "_ports";
|
|
2319
|
+
const echoBranch = canEcho
|
|
2320
|
+
? `\n if echo == "items" {\n return Some(${rowT} { val: ${portsParam}.${portFieldName("items")}.clone(), ..Default::default() });\n }`
|
|
2321
|
+
: "";
|
|
2322
|
+
const okBind = canEcho ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
|
|
2323
|
+
methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
|
|
2324
|
+
let ${okBind} = self.next(${rustStrLit(node.component)})?;
|
|
2209
2325
|
if is_err {
|
|
2210
2326
|
return Some(${rowT} { is_error: true, err, ..Default::default() });
|
|
2211
|
-
}
|
|
2327
|
+
}${echoBranch}
|
|
2212
2328
|
let v = ok.unwrap_or(Value::Null);
|
|
2213
2329
|
Some(${decode}(&v))
|
|
2214
2330
|
}`);
|
|
@@ -2263,19 +2379,22 @@ impl ScriptedNativeRaw {
|
|
|
2263
2379
|
|
|
2264
2380
|
// drain one scripted outcome for a component (last entry repeats — mirrors the reference scripted
|
|
2265
2381
|
// handlers); None when the component has no scripted queue (fail-closed). Returns (is_error, err,
|
|
2266
|
-
// ok_value). The bc#87 parallel-stage runner calls node_* from multiple threads, so the drain
|
|
2267
|
-
// Mutex-guarded.
|
|
2268
|
-
|
|
2382
|
+
// ok_value, echo). The bc#87 parallel-stage runner calls node_* from multiple threads, so the drain
|
|
2383
|
+
// is Mutex-guarded. #129: \`echo\` carries the reference scriptedHandlers' \`echo\` directive (e.g.
|
|
2384
|
+
// "items") so a scripted node can ECHO a named input PORT back — used to make the node→leaf array
|
|
2385
|
+
// dataflow test non-vacuous (the leaf returns its \`items\` port; a dropped/boxed array diverges).
|
|
2386
|
+
fn next(&self, component: &str) -> Option<(bool, String, Option<Value>, String)> {
|
|
2269
2387
|
self.seen.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
2270
2388
|
let mut queues = self.queues.lock().unwrap();
|
|
2271
2389
|
let (queue, single) = queues.get_mut(component)?;
|
|
2272
2390
|
let raw = if *single || queue.len() <= 1 { queue[0].clone() } else { queue.remove(0) };
|
|
2391
|
+
let echo = raw.get("echo").and_then(|e| e.as_str()).unwrap_or("").to_string();
|
|
2273
2392
|
if let Some(okv) = raw.get("ok") {
|
|
2274
2393
|
let v = behavior_contracts::decode_value(okv).expect("decode ok");
|
|
2275
|
-
Some((false, String::new(), Some(v)))
|
|
2394
|
+
Some((false, String::new(), Some(v), echo))
|
|
2276
2395
|
} else {
|
|
2277
2396
|
let e = raw.get("error").and_then(|e| e.as_str()).unwrap_or("").to_string();
|
|
2278
|
-
Some((true, e, None))
|
|
2397
|
+
Some((true, e, None, echo))
|
|
2279
2398
|
}
|
|
2280
2399
|
}
|
|
2281
2400
|
}`;
|
|
@@ -2442,6 +2561,7 @@ function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix)
|
|
|
2442
2561
|
}
|
|
2443
2562
|
export const rustTypedNativeEmitter = {
|
|
2444
2563
|
language: "rust-typed-native",
|
|
2564
|
+
classification: "codegen", // native: 型付きフラットコード生成・IR/dict 非在(#128/A6)
|
|
2445
2565
|
fileExtension: ".rs",
|
|
2446
2566
|
defaultRuntimeImport: "behavior_contracts",
|
|
2447
2567
|
filenameHint: "behaviors_typed_native_generated.rs",
|