behavior-contracts 0.8.0 → 0.8.2
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 +3 -0
- package/dist/authoring.d.ts.map +1 -1
- package/dist/authoring.js +7 -0
- package/dist/authoring.js.map +1 -1
- package/dist/behavior.d.ts +15 -0
- package/dist/behavior.d.ts.map +1 -1
- package/dist/behavior.js +26 -0
- package/dist/behavior.js.map +1 -1
- package/dist/expr-operator-types.d.ts.map +1 -1
- package/dist/expr-operator-types.js +2 -0
- package/dist/expr-operator-types.js.map +1 -1
- package/dist/generator/emit-shared-go-typed.d.ts.map +1 -1
- package/dist/generator/emit-shared-go-typed.js +130 -4
- package/dist/generator/emit-shared-go-typed.js.map +1 -1
- package/dist/generator/emit-shared-rust-typed.d.ts.map +1 -1
- package/dist/generator/emit-shared-rust-typed.js +49 -1
- package/dist/generator/emit-shared-rust-typed.js.map +1 -1
- package/dist/generator/emit-shared-typescript-typed.d.ts +83 -0
- package/dist/generator/emit-shared-typescript-typed.d.ts.map +1 -0
- package/dist/generator/emit-shared-typescript-typed.js +259 -0
- package/dist/generator/emit-shared-typescript-typed.js.map +1 -0
- package/dist/generator/emit-shared-typescript.d.ts.map +1 -1
- package/dist/generator/emit-shared-typescript.js +88 -0
- package/dist/generator/emit-shared-typescript.js.map +1 -1
- package/dist/generator/emit-straightline-typed-typescript.d.ts.map +1 -1
- package/dist/generator/emit-straightline-typed-typescript.js +2 -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 +250 -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 +244 -25
- package/dist/generator/emit-typed-native-rust.js.map +1 -1
- package/dist/generator/emit-typescript-native.d.ts +56 -0
- package/dist/generator/emit-typescript-native.d.ts.map +1 -0
- package/dist/generator/emit-typescript-native.js +44 -0
- package/dist/generator/emit-typescript-native.js.map +1 -0
- package/dist/generator/index.d.ts +2 -0
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +6 -0
- package/dist/generator/index.js.map +1 -1
- package/dist/generator/native-expr.d.ts +17 -0
- package/dist/generator/native-expr.d.ts.map +1 -1
- package/dist/generator/native-expr.js +82 -2
- package/dist/generator/native-expr.js.map +1 -1
- package/dist/generator/straightline.d.ts +22 -1
- package/dist/generator/straightline.d.ts.map +1 -1
- package/dist/generator/straightline.js +25 -7
- package/dist/generator/straightline.js.map +1 -1
- package/dist/generator/typed.d.ts +5 -0
- package/dist/generator/typed.d.ts.map +1 -1
- package/dist/generator/typed.js +8 -0
- package/dist/generator/typed.js.map +1 -1
- package/dist/guard.d.ts +2 -1
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +6 -5
- package/dist/guard.js.map +1 -1
- package/dist/type-gate.d.ts +1 -1
- package/dist/type-gate.d.ts.map +1 -1
- package/dist/type-gate.js +45 -24
- package/dist/type-gate.js.map +1 -1
- package/package.json +1 -1
|
@@ -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";
|
|
@@ -137,12 +138,12 @@ function inferPortType(node, inputPorts, asBinding) {
|
|
|
137
138
|
if (e.path.length === 1) {
|
|
138
139
|
const s = inputPorts[e.path[0]];
|
|
139
140
|
if (s) {
|
|
140
|
-
if (s.type === "string")
|
|
141
|
-
return "String";
|
|
141
|
+
if (s.type === "string" || s.type === "literal")
|
|
142
|
+
return "String"; // literal = constrained String.
|
|
142
143
|
if (s.type === "int")
|
|
143
144
|
return "i64";
|
|
144
|
-
if (s.type === "float")
|
|
145
|
-
return "f64";
|
|
145
|
+
if (s.type === "float" || s.type === "number")
|
|
146
|
+
return "f64"; // `"number"` = the float channel.
|
|
146
147
|
if (s.type === "bool")
|
|
147
148
|
return "bool";
|
|
148
149
|
}
|
|
@@ -192,12 +193,84 @@ function portFieldRustType(node, inputPorts, where = "port", asBinding, plan, ty
|
|
|
192
193
|
const num = numLiteralRustExpr(node);
|
|
193
194
|
if (num !== null)
|
|
194
195
|
return typeof node === "number" && Number.isInteger(node) ? "i64" : "f64";
|
|
196
|
+
// a ref to a `$as` element field (or an input port) whose type is a MAP or NAMED struct (NOT a bare
|
|
197
|
+
// scalar) → the native map/struct type. #108-map: a `{map:V}`-typed field (e.g. a nested
|
|
198
|
+
// `map<map<obj>>` write port) lowers to BTreeMap; a named field lowers to its struct. Reuses the same
|
|
199
|
+
// typedFieldAccess resolution as the scalar path; a genuinely-untyped ref still falls through.
|
|
200
|
+
const composite = portCompositeRef(node, inputPorts, asBinding, plan);
|
|
201
|
+
if (composite !== null)
|
|
202
|
+
return renderTypeRef(composite);
|
|
195
203
|
const ft = inferPortType(node, inputPorts, asBinding);
|
|
196
204
|
if (ft === "Value") {
|
|
197
205
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90 / runtime-free): ${where} '${JSON.stringify(node)}' does not lower to a native Rust type (would need a boxed Value). The covered read plane is runtime-free and admits NO boxed escape — the covered corpus ports are string / bool / static-string-array (projection) / bare-number (limit) only. Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
|
|
198
206
|
}
|
|
199
207
|
return ft;
|
|
200
208
|
}
|
|
209
|
+
/** portCompositeRef — the full TypeRef of a port that is a `{ref:[...]}` into a `$as` element field OR
|
|
210
|
+
* an input port whose type is a MAP or NAMED struct (a non-scalar covered field). Returns null when the
|
|
211
|
+
* node is not such a ref (so the caller falls through to the scalar/array inference). #108-map: enables a
|
|
212
|
+
* `{map:V}`-typed field port (e.g. a nested map write port) to lower to the native BTreeMap. */
|
|
213
|
+
function portCompositeRef(node, inputPorts, asBinding, plan) {
|
|
214
|
+
const e = classifyExpr(node);
|
|
215
|
+
if (e.kind !== "ref" || plan === undefined)
|
|
216
|
+
return null;
|
|
217
|
+
let ref = null;
|
|
218
|
+
if (asBinding !== undefined && e.path[0] === asBinding.name) {
|
|
219
|
+
try {
|
|
220
|
+
ref = typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan).ref;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else if (e.path.length >= 1) {
|
|
227
|
+
const s = inputPorts[e.path[0]];
|
|
228
|
+
// a whole-port ref to an input MAP port carrying an elemType → its BTreeMap type (mirrors
|
|
229
|
+
// inputPortRustType); a deeper path into an input port is not resolved here (no field type source).
|
|
230
|
+
if (s !== undefined && s.type === "map" && e.path.length === 1) {
|
|
231
|
+
const et = s.elemType;
|
|
232
|
+
if (et !== undefined)
|
|
233
|
+
return { kind: "map", value: deriveTypeRef(et, plan) };
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
if (ref === null)
|
|
238
|
+
return null;
|
|
239
|
+
// only MAP / NAMED composite fields here — scalars/arrays keep their existing dedicated paths.
|
|
240
|
+
if (ref.kind === "map" || ref.kind === "named")
|
|
241
|
+
return ref;
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
/** emitCompositePortValue — the OWNED native value expr for a port that is a ref to a `$as` element
|
|
245
|
+
* MAP/NAMED field (or an input map port): a typed field access `.clone()` (the concrete BTreeMap/struct,
|
|
246
|
+
* ZERO boxed Value). `elemBaseExpr` is the map arm's element base (`oel_<mapId>`) for a `$as`-headed ref;
|
|
247
|
+
* omitted at the top level (only input-port composite refs resolve there). Returns null otherwise. */
|
|
248
|
+
function emitCompositePortValue(node, inputPorts, asBinding, plan, elemBaseExpr) {
|
|
249
|
+
const e = classifyExpr(node);
|
|
250
|
+
if (e.kind !== "ref")
|
|
251
|
+
return null;
|
|
252
|
+
if (asBinding !== undefined && elemBaseExpr !== undefined && e.path[0] === asBinding.name) {
|
|
253
|
+
let acc;
|
|
254
|
+
try {
|
|
255
|
+
acc = typedFieldAccess(elemBaseExpr, asBinding.ref, e.path.slice(1), plan);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
if (acc.ref.kind !== "map" && acc.ref.kind !== "named")
|
|
261
|
+
return null;
|
|
262
|
+
return `${acc.expr}.clone()`;
|
|
263
|
+
}
|
|
264
|
+
// a WHOLE-port ref to an input MAP port (with elemType) — valid at top level OR inside a map node (a
|
|
265
|
+
// ref head that is NOT the `$as` element binding). Clones the native BTreeMap off the input struct.
|
|
266
|
+
if ((asBinding === undefined || e.path[0] !== asBinding.name) && e.path.length === 1) {
|
|
267
|
+
const s = inputPorts[e.path[0]];
|
|
268
|
+
if (s !== undefined && s.type === "map" && s.elemType !== undefined) {
|
|
269
|
+
return `in_.${rustFieldName(e.path[0])}.clone()`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
201
274
|
/**
|
|
202
275
|
* emitPortsStruct — the CONCRETE native ports struct for a componentRef node. Typed fields per the
|
|
203
276
|
* static port type, constructed directly. NO `impl PortReaderT` / by-name `port()` — the covered runner
|
|
@@ -316,6 +389,12 @@ function makeRustExprBackend(resolveHead, plan) {
|
|
|
316
389
|
notOp: (a) => `(!${a})`,
|
|
317
390
|
structLit: (name, inits) => `${name} { ${inits.map((i) => `${rustFieldName(i.field)}: ${i.expr}`).join(", ")} }`,
|
|
318
391
|
arrLit: (elemTy, items) => `{ let __v: Vec<${elemTy}> = vec![${items.join(", ")}]; __v }`,
|
|
392
|
+
// an opt (`Option<T>`) NONE literal. Annotate as `Option::<T>::None` so it type-infers in ANY
|
|
393
|
+
// position (a bare `None` can be ambiguous; the explicit turbofish keeps it a fully-typed value).
|
|
394
|
+
optNone: (innerTy) => `Option::<${innerTy}>::None`,
|
|
395
|
+
// opt (`Option<T>`) null-presence test: present = `expr.is_some()`, absent = `expr.is_none()`.
|
|
396
|
+
optIsSome: (expr) => `${expr}.is_some()`,
|
|
397
|
+
optIsNone: (expr) => `${expr}.is_none()`,
|
|
319
398
|
ternary: (cond, t, e, _ty) => `(if ${rustStripOuterParens(cond)} { ${rustStripOuterParens(t)} } else { ${rustStripOuterParens(e)} })`,
|
|
320
399
|
shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
|
|
321
400
|
// and: let temp = if left { <rStmts> rExpr } else { false }; or: if left { true } else { <rStmts> rExpr }.
|
|
@@ -592,6 +671,19 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
592
671
|
function mapElemMaterializerName(nodeId) {
|
|
593
672
|
return `materialize_map_elem_${typedCell(nodeId)}`;
|
|
594
673
|
}
|
|
674
|
+
/** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType`, normalizing the two IR
|
|
675
|
+
* conventions to the one array shape every downstream site assumes (`typedNodes.get(mapId)` is an arr):
|
|
676
|
+
* - the RECORDER SoT (authoring.ts recordMap): `node.outType` is the ELEMENT (pre-array-wrap) — wrap
|
|
677
|
+
* it to `{arr:element}` here (the map produces `[]element`), matching type-gate's outputType wrap
|
|
678
|
+
* (`"map" in n ? {arr:ot} : ot`). This is the declarative-authoring form (graphddb batchWrite/forEach).
|
|
679
|
+
* - legacy hand-built IR: `node.outType` is already the `{arr:element}` produced form — take it as-is.
|
|
680
|
+
* A map ELEMENT is never itself an `{arr:…}` in the covered corpus, so the `arr`-keyed discriminator is
|
|
681
|
+
* unambiguous. Both converge to the SAME produced-array ref, so existing goldens stay byte-identical. */
|
|
682
|
+
function mapNodeArrRef(node, plan) {
|
|
683
|
+
const outT = node.outType;
|
|
684
|
+
const isArr = outT !== null && typeof outT === "object" && "arr" in outT;
|
|
685
|
+
return deriveTypeRef((isArr ? outT : { arr: outT }), plan);
|
|
686
|
+
}
|
|
595
687
|
/** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
|
|
596
688
|
* type (into) or the element outType directly (no-into). This is the type of RawElemNR<Comp><Node>. */
|
|
597
689
|
function mapElemRowRef(node, arrRef, plan) {
|
|
@@ -828,6 +920,13 @@ function coveredMapNode(n, bodyIds, comp) {
|
|
|
828
920
|
return false;
|
|
829
921
|
if (m.relationKind !== undefined && m.relationKind !== "single" && m.relationKind !== "connection")
|
|
830
922
|
return false;
|
|
923
|
+
// A map node must carry an outType (its ELEMENT type per the recorder SoT — authoring.ts recordMap
|
|
924
|
+
// holds `node.outType = mapElem`, the PRE-array-wrap element; the emitter synthesizes the produced
|
|
925
|
+
// `[]element`). A map WITHOUT outType is uncovered. (Legacy hand-built IR that already carries the
|
|
926
|
+
// `{arr:element}` produced form is ALSO accepted — mapNodeElemRef normalizes both to the element.)
|
|
927
|
+
const outT = n.outType;
|
|
928
|
+
if (outT === undefined || typeof outT !== "object")
|
|
929
|
+
return false;
|
|
831
930
|
const overE = classifyExpr(m.over);
|
|
832
931
|
if (overE.kind !== "ref")
|
|
833
932
|
return false;
|
|
@@ -926,14 +1025,30 @@ function coverageRejectReason(comp) {
|
|
|
926
1025
|
// shape). Kept fail-closed: a non-componentRef member is rejected.
|
|
927
1026
|
for (const [i] of ep.parallelStageOf) {
|
|
928
1027
|
const n = comp.body[i];
|
|
1028
|
+
// #125/#108: a cond node IS allowed as a parallel-stage member — it is a pure Expression (no
|
|
1029
|
+
// dispatch), settled in ascending preflight order, so its produced flag / typed result cell is
|
|
1030
|
+
// settled before any dispatch. No handler, no boxed result — it does not disturb the bounded-
|
|
1031
|
+
// concurrency error-precedence protocol. Covers BOTH the when(...) control-gate cond (#136,
|
|
1032
|
+
// {ok}/null skip marker) AND the Φ-merge VALUE cond (#108, has outType — the sibling Φ-join). A
|
|
1033
|
+
// value cond's outType requirement is enforced by the per-node cond check below, so a parallel
|
|
1034
|
+
// value cond without outType still loud-rejects there.
|
|
1035
|
+
if ("cond" in n)
|
|
1036
|
+
continue;
|
|
929
1037
|
if (!("component" in n))
|
|
930
1038
|
return `node '${n.id}' is a non-componentRef member of a parallel stage (not native-covered)`;
|
|
931
1039
|
}
|
|
932
1040
|
const bodyIds = new Set(comp.body.map((b) => b.id));
|
|
933
1041
|
for (const n of comp.body) {
|
|
934
1042
|
if ("cond" in n) {
|
|
935
|
-
// #
|
|
936
|
-
//
|
|
1043
|
+
// #125: a when(...) control-gate cond (`{if, then:{obj:{ok:true}}, else:null}`) は制御スキップ
|
|
1044
|
+
// マーカーで出力データ型を持たない(A3 の typeless design)— outType を要求しない。gate は `if` を
|
|
1045
|
+
// native bool へコンパイルし produced_<gate> に写すだけで、分岐内ノードは既存の
|
|
1046
|
+
// `parent=gate, bindField:"ok"` skip-gate(run_behavior の null-binding skip)が実行選択する。
|
|
1047
|
+
if (isControlGate(n))
|
|
1048
|
+
continue;
|
|
1049
|
+
// #108: a DATA-JOIN cond node (Φ 合流) is native-covered (its if/then/else compile via the runtime-
|
|
1050
|
+
// free NativeExprCompiler; an uncovered expr fails closed at emit). A data-join cond requires outType
|
|
1051
|
+
// (合流値を de-box する対象だから — control gate だけがこの要求を免除される)。
|
|
937
1052
|
if (n.outType === undefined)
|
|
938
1053
|
return `node '${n.id}' cond has no outType (required to de-box the branch join)`;
|
|
939
1054
|
continue;
|
|
@@ -953,7 +1068,12 @@ function coverageRejectReason(comp) {
|
|
|
953
1068
|
const cr = n;
|
|
954
1069
|
if (cr.policy !== undefined && cr.policy !== "fail")
|
|
955
1070
|
return `node '${n.id}' componentRef policy '${cr.policy}' (produced-aware output not yet native-covered)`;
|
|
956
|
-
|
|
1071
|
+
// #125: a when(...) control-gate child (`parent=gate, bindField:"ok"`). Its bindField is the gate's
|
|
1072
|
+
// skip signal, NOT a relation data key — so the relationKind constraint below (which governs a
|
|
1073
|
+
// relation bindField into a single|connection parent) does not apply. Its skip is driven by the
|
|
1074
|
+
// gate's produced_<gate> flag; the child itself can be a plain point read (or a relation read).
|
|
1075
|
+
const childOfGate = cr.parent !== undefined && isControlGate(comp.body.find((b) => b.id === cr.parent));
|
|
1076
|
+
if (!childOfGate && cr.bindField !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
957
1077
|
return `node '${n.id}' relationKind '${cr.relationKind ?? "point"}' not native-covered (bindField only supports single|connection)`;
|
|
958
1078
|
if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
959
1079
|
return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
|
|
@@ -968,8 +1088,10 @@ function coverageRejectReason(comp) {
|
|
|
968
1088
|
function isFullyTyped(comp) {
|
|
969
1089
|
if (comp.outputType === undefined)
|
|
970
1090
|
return false;
|
|
1091
|
+
// #125: a control-gate cond is a typeless skip marker (A3) — it never de-boxes a result, so it is
|
|
1092
|
+
// exempt from the "every node carries outType" de-box requirement (matches type-gate's exemption).
|
|
971
1093
|
for (const n of comp.body)
|
|
972
|
-
if (n.outType === undefined)
|
|
1094
|
+
if (!isControlGate(n) && n.outType === undefined)
|
|
973
1095
|
return false;
|
|
974
1096
|
return true;
|
|
975
1097
|
}
|
|
@@ -988,10 +1110,16 @@ function assertTypedCoverage(comp) {
|
|
|
988
1110
|
function inputPortRustType(schema, plan) {
|
|
989
1111
|
switch (schema?.type) {
|
|
990
1112
|
case "string":
|
|
1113
|
+
// a `"literal"` port is a constrained STRING union (`param.literal('a','b',…)`, stored 'S') — its
|
|
1114
|
+
// native representation is a plain String (the union constraint is a validation concern, not a type).
|
|
1115
|
+
case "literal":
|
|
991
1116
|
return "String";
|
|
992
1117
|
case "int":
|
|
993
1118
|
return "i64";
|
|
994
1119
|
case "float":
|
|
1120
|
+
// a `"number"` port is a FLOAT (run_behavior: `typeName(number) === "float"`, expr-eval.ts §value):
|
|
1121
|
+
// a JS number is IEEE754 → f64. (An i64 value is declared `"int"`; `"number"` is the float channel.)
|
|
1122
|
+
case "number":
|
|
995
1123
|
return "f64";
|
|
996
1124
|
case "bool":
|
|
997
1125
|
return "bool";
|
|
@@ -1003,6 +1131,13 @@ function inputPortRustType(schema, plan) {
|
|
|
1003
1131
|
return `Vec<${renderTypeRef(deriveTypeRef(et, plan))}>`;
|
|
1004
1132
|
return "Value";
|
|
1005
1133
|
}
|
|
1134
|
+
case "map": {
|
|
1135
|
+
// an input MAP port (dynamic string keys, declared value elemType) lowers to a native BTreeMap.
|
|
1136
|
+
const et = schema.elemType;
|
|
1137
|
+
if (et !== undefined && plan !== undefined)
|
|
1138
|
+
return `std::collections::BTreeMap<String, ${renderTypeRef(deriveTypeRef(et, plan))}>`;
|
|
1139
|
+
return "Value";
|
|
1140
|
+
}
|
|
1006
1141
|
default:
|
|
1007
1142
|
return "Value";
|
|
1008
1143
|
}
|
|
@@ -1011,10 +1146,12 @@ function inputPortRustType(schema, plan) {
|
|
|
1011
1146
|
function inputScalarKind(schema) {
|
|
1012
1147
|
switch (schema?.type) {
|
|
1013
1148
|
case "string":
|
|
1149
|
+
case "literal": // a `"literal"` union is a constrained String (see inputPortRustType).
|
|
1014
1150
|
return "String";
|
|
1015
1151
|
case "int":
|
|
1016
1152
|
return "i64";
|
|
1017
1153
|
case "float":
|
|
1154
|
+
case "number": // a `"number"` port is the FLOAT channel (run_behavior: number → float / f64).
|
|
1018
1155
|
return "f64";
|
|
1019
1156
|
case "bool":
|
|
1020
1157
|
return "bool";
|
|
@@ -1111,8 +1248,16 @@ function emitNativeScalar(node, want, resolveRef) {
|
|
|
1111
1248
|
* a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
|
|
1112
1249
|
* ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
|
|
1113
1250
|
*/
|
|
1114
|
-
function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan, typedNodes) {
|
|
1251
|
+
function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan, typedNodes, asBinding, elemBaseExpr) {
|
|
1115
1252
|
const field = portFieldName(pn);
|
|
1253
|
+
// #108-map: a port that is a ref to a `$as` element MAP/NAMED field (or an input map port) → CLONE the
|
|
1254
|
+
// typed composite value directly off the element/input (native BTreeMap / struct — ZERO boxed Value).
|
|
1255
|
+
// Reuses portCompositeRef for the type; the value is the typed field access `.clone()`.
|
|
1256
|
+
if (plan !== undefined) {
|
|
1257
|
+
const compExpr = emitCompositePortValue(expr, inputPorts ?? {}, asBinding, plan, elemBaseExpr);
|
|
1258
|
+
if (compExpr !== null)
|
|
1259
|
+
return `${field}: ${compExpr}`;
|
|
1260
|
+
}
|
|
1116
1261
|
// #110: a componentRef port bound to a runtime array input port (with elemType) → the native Vec<ElemT>,
|
|
1117
1262
|
// cloned out of the concrete input struct (`in_.<field>.clone()`) — ZERO boxed Value on the read hot path.
|
|
1118
1263
|
if (inputPorts !== undefined && portArrayElemRef(expr, inputPorts, plan) !== null) {
|
|
@@ -1212,8 +1357,11 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1212
1357
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native async (bc#97): component '${comp.name}' has a bc#87 real-concurrency stage AND an async terminal handler, but a scoped-std-thread stage cannot host \`.await\`. Async + concurrent-stage (join! over the stage's futures) is a follow-up — declare this component's terminals sync, or split the concurrency stage.`);
|
|
1213
1358
|
}
|
|
1214
1359
|
const typedNodes = new Map();
|
|
1360
|
+
// #125: control-gate は typeless(typedNodes に載せない)。map ノードは produced-array ref に正規化
|
|
1361
|
+
// (recorder SoT の element outType を {arr:element} へラップ / legacy {arr} はそのまま)。
|
|
1215
1362
|
for (const n of comp.body)
|
|
1216
|
-
|
|
1363
|
+
if (!isControlGate(n))
|
|
1364
|
+
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
|
|
1217
1365
|
const idToIndex = new Map();
|
|
1218
1366
|
comp.body.forEach((n, i) => idToIndex.set(n.id, i));
|
|
1219
1367
|
const opsLiteral = comp.body.map((n) => {
|
|
@@ -1306,8 +1454,13 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1306
1454
|
lines.push(` ${inParam}: ${inStructName(comp.name)},`);
|
|
1307
1455
|
lines.push(`) -> Result<${outTy}, BehaviorError> {`);
|
|
1308
1456
|
for (const k of order.keys()) {
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1457
|
+
const nd = comp.body[order[k]];
|
|
1458
|
+
const id = nd.id;
|
|
1459
|
+
// #125: a control-gate cond has no typed data cell — only a produced_<gate> flag (its `if` result),
|
|
1460
|
+
// which its gated children read via the existing `if produced_<parent>.get()` skip-gate.
|
|
1461
|
+
if (!isControlGate(nd)) {
|
|
1462
|
+
lines.push(` let ${typedCell(id)}: RefCell<${renderTypeRef(typedNodes.get(id))}> = RefCell::new(Default::default());`);
|
|
1463
|
+
}
|
|
1311
1464
|
lines.push(` let produced_${sanitize(id)} = std::cell::Cell::new(false);`);
|
|
1312
1465
|
lines.push(` let _ = &produced_${sanitize(id)};`);
|
|
1313
1466
|
}
|
|
@@ -1341,11 +1494,19 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1341
1494
|
lines.push(` // ── op '${node.id}' (${node.component}${node.relationKind ? ", relationKind:" + node.relationKind : ""}${node.parent ? ", parent:" + node.parent : ""}) ──`);
|
|
1342
1495
|
let indent = " ";
|
|
1343
1496
|
const closers = [];
|
|
1497
|
+
const parentIsGate = node.parent !== undefined && isControlGate(comp.body.find((b) => b.id === node.parent));
|
|
1344
1498
|
if (node.parent !== undefined && priorNodeAt(node.parent, k)) {
|
|
1345
1499
|
lines.push(`${indent}if produced_${sanitize(node.parent)}.get() {`);
|
|
1346
1500
|
closers.unshift(`${indent}}`);
|
|
1347
1501
|
indent += " ";
|
|
1348
|
-
if (node.bindField !== undefined) {
|
|
1502
|
+
if (node.bindField !== undefined && parentIsGate) {
|
|
1503
|
+
// #125: a when(...) control-gate parent. gate の bindField:"ok" は run_behavior では成立時 `true`
|
|
1504
|
+
// / 不成立時 `null`(→ null-binding skip)。native では produced_<gate>(= gate の `if`)が既に
|
|
1505
|
+
// その skip を担うので、ここに来た時点で gate は成立。bound は skip の sentinel(データキーでは
|
|
1506
|
+
// ない — 子の ports は実データ親を参照する)として、非 None の `Option<String>` を渡す。
|
|
1507
|
+
lines.push(`${indent}let bound_${s}: ${boundRustType(node, typedNodes, plan)} = Some("ok".to_string());`);
|
|
1508
|
+
}
|
|
1509
|
+
else if (node.bindField !== undefined) {
|
|
1349
1510
|
// bind (change #3 — typed produced-aware bound): read the parent struct field DIRECTLY (typed
|
|
1350
1511
|
// access — no serialize). `bound_<s>` is a typed `Option<K>` — None => UNPRODUCED/skip, Some =>
|
|
1351
1512
|
// produced (an empty string is a VALID produced value, `Some("")`). This is the EXACT `!= Null`
|
|
@@ -1436,17 +1597,41 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1436
1597
|
const ids = stage.map((i) => comp.body[i].id);
|
|
1437
1598
|
lines.push(` // ── bc#87 real-concurrency stage {${ids.map((x) => rustStrLit(x)).join(", ")}} — static parallel`);
|
|
1438
1599
|
lines.push(` // orchestration (scoped threads, bound=${concurrency}); preflight+commit in ascending index order.`);
|
|
1600
|
+
// #125/#108: a stage member may be a cond node — a pure Expression (NO handler/dispatch). The scoped-
|
|
1601
|
+
// thread orchestration governs only the componentRef fan-out members; a cond is settled in ascending
|
|
1602
|
+
// preflight order — BEFORE any dispatch — via the SAME emitCondArm the sequential path uses (no forked
|
|
1603
|
+
// lowering). emitCondArm routes internally: a when(...) control-gate cond (#136, isControlGate, no
|
|
1604
|
+
// outType) commits produced_<gate> ({ok}/null skip marker); a Φ-merge VALUE cond (#108, has outType,
|
|
1605
|
+
// then/else are data branches) materializes its typed result cell (the sibling Φ-join — e.g. two
|
|
1606
|
+
// when-gated connection relations each folding to `ref | emptyConn`). Both are mutually independent
|
|
1607
|
+
// (§1), so ascending-at-head is byte-equal to run_behavior. A cond never shares a stage with a node
|
|
1608
|
+
// that depends on it (an intra-stage parent/child dep is rejected by concurrencyPlan).
|
|
1609
|
+
const condMembers = stage.filter((i) => "cond" in comp.body[i]);
|
|
1610
|
+
const refMembers = stage.filter((i) => !("cond" in comp.body[i]));
|
|
1611
|
+
for (const i of condMembers) {
|
|
1612
|
+
lines.push(emitCondArm(comp, comp.body[i], atPos + stage.indexOf(i), typedNodes, plan, priorNodeAt));
|
|
1613
|
+
}
|
|
1614
|
+
// A cond-ONLY stage carries no handler fan-out — the conds above ARE the whole stage. Skip the scoped-
|
|
1615
|
+
// thread orchestration block entirely (an empty one is dead scaffolding clippy rejects: never_loop /
|
|
1616
|
+
// match_single_binding on a zero-job scope).
|
|
1617
|
+
if (refMembers.length === 0)
|
|
1618
|
+
return lines.join("\n");
|
|
1439
1619
|
lines.push(` {`);
|
|
1440
1620
|
// 1) PREFLIGHT (ascending): build each member's CONCRETE ports struct into a stage-scoped Option
|
|
1441
1621
|
// local. #114: a bindField relation child ALSO reads its produced-aware bound key from the SETTLED
|
|
1442
1622
|
// parent struct field (typed access — no serialize) into a stage-scoped Option; a None bound =>
|
|
1443
1623
|
// UNPRODUCED/skip (ports_<s> stays None), exactly run_behavior's preflightOp bindField skip, before
|
|
1444
1624
|
// any dispatch. The bound is captured (cloned) by the member's scoped-thread worker.
|
|
1445
|
-
for (const i of
|
|
1625
|
+
for (const i of refMembers) {
|
|
1446
1626
|
const node = comp.body[i];
|
|
1447
1627
|
const s = sanitize(node.id);
|
|
1448
1628
|
const structName = portsStructName(comp.name, node.id);
|
|
1449
1629
|
const gated = node.parent !== undefined && priorNodeAt(node.parent, atPos);
|
|
1630
|
+
// #125: a member gated by a PRIOR-stage control gate (`parent=gate, bindField:"ok"`). Its bindField is
|
|
1631
|
+
// the gate's skip sentinel, NOT a relation data key — the gate is typeless (no struct field to read).
|
|
1632
|
+
// Mirror the sequential arm: past the produced_<gate> gate we are past the skip, so the bound is the
|
|
1633
|
+
// non-None "ok" sentinel (the child's ports read the REAL data parent, not the gate).
|
|
1634
|
+
const parentIsGate = node.parent !== undefined && isControlGate(comp.body.find((b) => b.id === node.parent));
|
|
1450
1635
|
let ind = " ";
|
|
1451
1636
|
const closers = [];
|
|
1452
1637
|
if (node.bindField !== undefined) {
|
|
@@ -1458,7 +1643,10 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1458
1643
|
lines.push(`${ind}if produced_${sanitize(node.parent)}.get() {`);
|
|
1459
1644
|
closers.unshift(`${ind}}`);
|
|
1460
1645
|
ind += " ";
|
|
1461
|
-
if (node.bindField !== undefined) {
|
|
1646
|
+
if (node.bindField !== undefined && parentIsGate) {
|
|
1647
|
+
lines.push(`${ind}bound_${s} = Some("ok".to_string());`);
|
|
1648
|
+
}
|
|
1649
|
+
else if (node.bindField !== undefined) {
|
|
1462
1650
|
const parentRef = typedNodes.get(node.parent);
|
|
1463
1651
|
const { expr: bfExpr, ref: bfRef } = typedFieldAccess(`${typedCell(node.parent)}.borrow()`, parentRef, [node.bindField], plan);
|
|
1464
1652
|
if (bfRef.kind === "opt") {
|
|
@@ -1491,16 +1679,18 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1491
1679
|
}
|
|
1492
1680
|
// 2) DISPATCH (bounded scoped threads). Share `&H` (concrete trait; H: Sync). Each worker calls the
|
|
1493
1681
|
// concrete node_* method for its member's CONCRETE ports and stores the concrete row in its slot.
|
|
1494
|
-
|
|
1682
|
+
// #125: the fan-out (slots / jobs / workers) spans the COMPONENTREF members only — a control gate
|
|
1683
|
+
// dispatches no handler (committed in preflight above), so it is not a scoped-thread job.
|
|
1684
|
+
const nMembers = refMembers.length;
|
|
1495
1685
|
const boundN = Math.min(concurrency, nMembers);
|
|
1496
1686
|
const jobsVar = `jobs_${sanitize(comp.name)}`;
|
|
1497
|
-
for (const i of
|
|
1687
|
+
for (const i of refMembers) {
|
|
1498
1688
|
const s = sanitize(comp.body[i].id);
|
|
1499
1689
|
const rowT = rawRowStructName(comp.name, comp.body[i].id);
|
|
1500
1690
|
lines.push(` let slot_${s}: std::sync::Mutex<Option<Option<${rowT}>>> = std::sync::Mutex::new(None);`);
|
|
1501
1691
|
}
|
|
1502
1692
|
lines.push(` let mut ${jobsVar}: Vec<usize> = Vec::new();`);
|
|
1503
|
-
|
|
1693
|
+
refMembers.forEach((i, slot) => {
|
|
1504
1694
|
const s = sanitize(comp.body[i].id);
|
|
1505
1695
|
lines.push(` if ports_${s}.is_some() { ${jobsVar}.push(${slot}); }`);
|
|
1506
1696
|
});
|
|
@@ -1513,7 +1703,7 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1513
1703
|
lines.push(` if j >= ${jobsVar}.len() { break; }`);
|
|
1514
1704
|
lines.push(` let slot = ${jobsVar}[j];`);
|
|
1515
1705
|
lines.push(` match slot {`);
|
|
1516
|
-
|
|
1706
|
+
refMembers.forEach((i, slot) => {
|
|
1517
1707
|
const node = comp.body[i];
|
|
1518
1708
|
const s = sanitize(node.id);
|
|
1519
1709
|
const boundArg = node.bindField !== undefined ? `bound_${s}.clone()` : "None";
|
|
@@ -1529,7 +1719,7 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1529
1719
|
lines.push(` }`);
|
|
1530
1720
|
lines.push(` });`);
|
|
1531
1721
|
// 3) COMMIT (ascending index): interpret + move the concrete row into the outType cell.
|
|
1532
|
-
|
|
1722
|
+
refMembers.forEach((i) => {
|
|
1533
1723
|
const node = comp.body[i];
|
|
1534
1724
|
const meta = metas[i];
|
|
1535
1725
|
const s = sanitize(node.id);
|
|
@@ -1641,7 +1831,8 @@ isAsync = false) {
|
|
|
1641
1831
|
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1642
1832
|
for (const pn of portNames) {
|
|
1643
1833
|
const ty = portFieldRustType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding, plan);
|
|
1644
|
-
|
|
1834
|
+
// #108-map: pass the element binding + base expr so a `$as` MAP/NAMED field port clones the typed composite.
|
|
1835
|
+
inits.push(emitPortInit(pn, m.ports[pn], ty, resolveRef, comp.inputPorts, plan, undefined, asBinding, `oel_${s}`));
|
|
1645
1836
|
}
|
|
1646
1837
|
out.push(`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
1647
1838
|
return out;
|
|
@@ -1910,7 +2101,6 @@ function rustInputHeadRef(head, comp, plan) {
|
|
|
1910
2101
|
function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
|
|
1911
2102
|
const c = node.cond;
|
|
1912
2103
|
const s = sanitize(node.id);
|
|
1913
|
-
const nodeRef = typedNodes.get(node.id);
|
|
1914
2104
|
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1915
2105
|
const resolveHead = (head) => {
|
|
1916
2106
|
if (priorHere(head))
|
|
@@ -1918,8 +2108,29 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
|
|
|
1918
2108
|
return rustInputHeadRef(head, comp, plan);
|
|
1919
2109
|
};
|
|
1920
2110
|
const compiler = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan));
|
|
1921
|
-
const resolveDecl = (name) => findDecl(plan, name).fields;
|
|
1922
2111
|
const cond = compiler.compileBool(c.if);
|
|
2112
|
+
// #125: a when(...) control-gate cond は出力データ型を持たない制御スキップマーカー。typed cell へ何も
|
|
2113
|
+
// materialize せず、`if` を native bool にして produced_<gate> に写すだけ。分岐内ノード
|
|
2114
|
+
// (`parent=gate, bindField:"ok"`)は既存の `if produced_<gate>.get()` skip-gate が実行選択する。
|
|
2115
|
+
if (isControlGate(node)) {
|
|
2116
|
+
const glines = [];
|
|
2117
|
+
glines.push(` // ── control-gate cond '${node.id}'${c.parent ? " (parent:" + c.parent + ")" : ""} — when(...) skip marker (no data type) ──`);
|
|
2118
|
+
let gindent = " ";
|
|
2119
|
+
const gclosers = [];
|
|
2120
|
+
if (c.parent !== undefined && priorNodeAt(c.parent, atPos)) {
|
|
2121
|
+
glines.push(`${gindent}if produced_${sanitize(c.parent)}.get() {`);
|
|
2122
|
+
gclosers.unshift(`${gindent}}`);
|
|
2123
|
+
gindent += " ";
|
|
2124
|
+
}
|
|
2125
|
+
for (const st of cond.stmts)
|
|
2126
|
+
glines.push(`${gindent}${st}`);
|
|
2127
|
+
glines.push(`${gindent}produced_${s}.set(${rustStripOuterParens(cond.expr)});`);
|
|
2128
|
+
for (const cl of gclosers)
|
|
2129
|
+
glines.push(cl);
|
|
2130
|
+
return glines.join("\n");
|
|
2131
|
+
}
|
|
2132
|
+
const nodeRef = typedNodes.get(node.id);
|
|
2133
|
+
const resolveDecl = (name) => findDecl(plan, name).fields;
|
|
1923
2134
|
const thenC = compiler.compileTo(c.then, nodeRef, resolveDecl);
|
|
1924
2135
|
const elseC = compiler.compileTo(c.else, nodeRef, resolveDecl);
|
|
1925
2136
|
const lines = [];
|
|
@@ -2079,7 +2290,8 @@ ${emitStructSurface([])}
|
|
|
2079
2290
|
const typedNodes = new Map();
|
|
2080
2291
|
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
2081
2292
|
for (const n of c.body)
|
|
2082
|
-
|
|
2293
|
+
if (!isControlGate(n))
|
|
2294
|
+
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan)); // #125: control-gate typeless; map は produced-array ref に正規化
|
|
2083
2295
|
for (const n of c.body) {
|
|
2084
2296
|
if ("fanout" in n)
|
|
2085
2297
|
structs.push(emitFanoutPortsStructs(c, n, typedNodes, plan));
|
|
@@ -2177,7 +2389,8 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2177
2389
|
const fnKwFor = (nodeId) => (ap.nodeIsAsync(nodeId) ? "async fn" : "fn");
|
|
2178
2390
|
const typedNodes = new Map();
|
|
2179
2391
|
for (const n of c.body)
|
|
2180
|
-
|
|
2392
|
+
if (!isControlGate(n))
|
|
2393
|
+
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan)); // #125: control-gate typeless; map は produced-array ref に正規化
|
|
2181
2394
|
const methods = [];
|
|
2182
2395
|
for (const n of c.body) {
|
|
2183
2396
|
if ("cond" in n)
|
|
@@ -2459,6 +2672,12 @@ function emitInDecoder(comp, plan) {
|
|
|
2459
2672
|
lines.push(` other => return Err(marshal_type_error(${rustStrLit(`decode input ${p}: expected array, got `)}, other.type_name())),`);
|
|
2460
2673
|
lines.push(` }`);
|
|
2461
2674
|
}
|
|
2675
|
+
else if (schema?.type === "map" && et !== undefined) {
|
|
2676
|
+
// #108-map: decode the boxed Value::Obj into the native BTreeMap<String, V> (test glue — the input
|
|
2677
|
+
// map port carries a declared elemType). materializeExpr covers the map de-box (obj → BTreeMap).
|
|
2678
|
+
const mapRef = { kind: "map", value: deriveTypeRef(et, plan) };
|
|
2679
|
+
lines.push(` in_.${field} = ${materializeExpr("v", mapRef, plan)};`);
|
|
2680
|
+
}
|
|
2462
2681
|
else if (scalar === undefined) {
|
|
2463
2682
|
lines.push(` in_.${field} = v.clone();`);
|
|
2464
2683
|
}
|