behavior-contracts 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring.d.ts +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 +4 -0
- package/dist/behavior.d.ts.map +1 -1
- 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 +179 -21
- 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 +192 -26
- 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.map +1 -1
- package/dist/type-gate.js +5 -2
- package/dist/type-gate.js.map +1 -1
- package/package.json +1 -1
|
@@ -138,12 +138,12 @@ function inferPortType(node, inputPorts, asBinding) {
|
|
|
138
138
|
if (e.path.length === 1) {
|
|
139
139
|
const s = inputPorts[e.path[0]];
|
|
140
140
|
if (s) {
|
|
141
|
-
if (s.type === "string")
|
|
142
|
-
return "String";
|
|
141
|
+
if (s.type === "string" || s.type === "literal")
|
|
142
|
+
return "String"; // literal = constrained String.
|
|
143
143
|
if (s.type === "int")
|
|
144
144
|
return "i64";
|
|
145
|
-
if (s.type === "float")
|
|
146
|
-
return "f64";
|
|
145
|
+
if (s.type === "float" || s.type === "number")
|
|
146
|
+
return "f64"; // `"number"` = the float channel.
|
|
147
147
|
if (s.type === "bool")
|
|
148
148
|
return "bool";
|
|
149
149
|
}
|
|
@@ -193,12 +193,84 @@ function portFieldRustType(node, inputPorts, where = "port", asBinding, plan, ty
|
|
|
193
193
|
const num = numLiteralRustExpr(node);
|
|
194
194
|
if (num !== null)
|
|
195
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);
|
|
196
203
|
const ft = inferPortType(node, inputPorts, asBinding);
|
|
197
204
|
if (ft === "Value") {
|
|
198
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.`);
|
|
199
206
|
}
|
|
200
207
|
return ft;
|
|
201
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
|
+
}
|
|
202
274
|
/**
|
|
203
275
|
* emitPortsStruct — the CONCRETE native ports struct for a componentRef node. Typed fields per the
|
|
204
276
|
* static port type, constructed directly. NO `impl PortReaderT` / by-name `port()` — the covered runner
|
|
@@ -317,6 +389,12 @@ function makeRustExprBackend(resolveHead, plan) {
|
|
|
317
389
|
notOp: (a) => `(!${a})`,
|
|
318
390
|
structLit: (name, inits) => `${name} { ${inits.map((i) => `${rustFieldName(i.field)}: ${i.expr}`).join(", ")} }`,
|
|
319
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()`,
|
|
320
398
|
ternary: (cond, t, e, _ty) => `(if ${rustStripOuterParens(cond)} { ${rustStripOuterParens(t)} } else { ${rustStripOuterParens(e)} })`,
|
|
321
399
|
shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
|
|
322
400
|
// and: let temp = if left { <rStmts> rExpr } else { false }; or: if left { true } else { <rStmts> rExpr }.
|
|
@@ -589,9 +667,26 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
589
667
|
lines.push(`}`);
|
|
590
668
|
return lines.join("\n");
|
|
591
669
|
}
|
|
592
|
-
/** mapElemMaterializerName — the augmented-element copier for a covered map...into node.
|
|
593
|
-
|
|
594
|
-
|
|
670
|
+
/** mapElemMaterializerName — the augmented-element copier for a covered map...into node.
|
|
671
|
+
* QUALIFIED by component name: two components may each carry a map node at the SAME
|
|
672
|
+
* recorder-scan index (e.g. both `…authorFollows` and `…groupsNested` have a map at `n4`),
|
|
673
|
+
* and a node-id-only name would collide at module scope (E0428). Mirrors the Go emitter
|
|
674
|
+
* (`materializeMapElemNR_<comp>_<node>`) and the sibling `decode_row_<comp>_<node>` helper. */
|
|
675
|
+
function mapElemMaterializerName(compName, nodeId) {
|
|
676
|
+
return `materialize_map_elem_${sanitize(compName)}_${typedCell(nodeId)}`;
|
|
677
|
+
}
|
|
678
|
+
/** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType`, normalizing the two IR
|
|
679
|
+
* conventions to the one array shape every downstream site assumes (`typedNodes.get(mapId)` is an arr):
|
|
680
|
+
* - the RECORDER SoT (authoring.ts recordMap): `node.outType` is the ELEMENT (pre-array-wrap) — wrap
|
|
681
|
+
* it to `{arr:element}` here (the map produces `[]element`), matching type-gate's outputType wrap
|
|
682
|
+
* (`"map" in n ? {arr:ot} : ot`). This is the declarative-authoring form (graphddb batchWrite/forEach).
|
|
683
|
+
* - legacy hand-built IR: `node.outType` is already the `{arr:element}` produced form — take it as-is.
|
|
684
|
+
* A map ELEMENT is never itself an `{arr:…}` in the covered corpus, so the `arr`-keyed discriminator is
|
|
685
|
+
* unambiguous. Both converge to the SAME produced-array ref, so existing goldens stay byte-identical. */
|
|
686
|
+
function mapNodeArrRef(node, plan) {
|
|
687
|
+
const outT = node.outType;
|
|
688
|
+
const isArr = outT !== null && typeof outT === "object" && "arr" in outT;
|
|
689
|
+
return deriveTypeRef((isArr ? outT : { arr: outT }), plan);
|
|
595
690
|
}
|
|
596
691
|
/** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
|
|
597
692
|
* type (into) or the element outType directly (no-into). This is the type of RawElemNR<Comp><Node>. */
|
|
@@ -634,7 +729,7 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
|
634
729
|
const intoField = decl.fields.find((f) => f.name === intoKey);
|
|
635
730
|
const overFields = decl.fields.filter((f) => f.name !== intoKey);
|
|
636
731
|
const overElemTy = mapOverElemType(comp, n, typedNodes, plan);
|
|
637
|
-
const fn = mapElemMaterializerName(n.id);
|
|
732
|
+
const fn = mapElemMaterializerName(comp.name, n.id);
|
|
638
733
|
const elemRowGo = rawElemStructName(comp.name, n.id);
|
|
639
734
|
const intoRowRef = mapElemRowRef(n, arrRef, plan);
|
|
640
735
|
const inits = [];
|
|
@@ -829,6 +924,13 @@ function coveredMapNode(n, bodyIds, comp) {
|
|
|
829
924
|
return false;
|
|
830
925
|
if (m.relationKind !== undefined && m.relationKind !== "single" && m.relationKind !== "connection")
|
|
831
926
|
return false;
|
|
927
|
+
// A map node must carry an outType (its ELEMENT type per the recorder SoT — authoring.ts recordMap
|
|
928
|
+
// holds `node.outType = mapElem`, the PRE-array-wrap element; the emitter synthesizes the produced
|
|
929
|
+
// `[]element`). A map WITHOUT outType is uncovered. (Legacy hand-built IR that already carries the
|
|
930
|
+
// `{arr:element}` produced form is ALSO accepted — mapNodeElemRef normalizes both to the element.)
|
|
931
|
+
const outT = n.outType;
|
|
932
|
+
if (outT === undefined || typeof outT !== "object")
|
|
933
|
+
return false;
|
|
832
934
|
const overE = classifyExpr(m.over);
|
|
833
935
|
if (overE.kind !== "ref")
|
|
834
936
|
return false;
|
|
@@ -927,11 +1029,14 @@ function coverageRejectReason(comp) {
|
|
|
927
1029
|
// shape). Kept fail-closed: a non-componentRef member is rejected.
|
|
928
1030
|
for (const [i] of ep.parallelStageOf) {
|
|
929
1031
|
const n = comp.body[i];
|
|
930
|
-
// #125: a
|
|
931
|
-
//
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
|
|
1032
|
+
// #125/#108: a cond node IS allowed as a parallel-stage member — it is a pure Expression (no
|
|
1033
|
+
// dispatch), settled in ascending preflight order, so its produced flag / typed result cell is
|
|
1034
|
+
// settled before any dispatch. No handler, no boxed result — it does not disturb the bounded-
|
|
1035
|
+
// concurrency error-precedence protocol. Covers BOTH the when(...) control-gate cond (#136,
|
|
1036
|
+
// {ok}/null skip marker) AND the Φ-merge VALUE cond (#108, has outType — the sibling Φ-join). A
|
|
1037
|
+
// value cond's outType requirement is enforced by the per-node cond check below, so a parallel
|
|
1038
|
+
// value cond without outType still loud-rejects there.
|
|
1039
|
+
if ("cond" in n)
|
|
935
1040
|
continue;
|
|
936
1041
|
if (!("component" in n))
|
|
937
1042
|
return `node '${n.id}' is a non-componentRef member of a parallel stage (not native-covered)`;
|
|
@@ -1009,10 +1114,16 @@ function assertTypedCoverage(comp) {
|
|
|
1009
1114
|
function inputPortRustType(schema, plan) {
|
|
1010
1115
|
switch (schema?.type) {
|
|
1011
1116
|
case "string":
|
|
1117
|
+
// a `"literal"` port is a constrained STRING union (`param.literal('a','b',…)`, stored 'S') — its
|
|
1118
|
+
// native representation is a plain String (the union constraint is a validation concern, not a type).
|
|
1119
|
+
case "literal":
|
|
1012
1120
|
return "String";
|
|
1013
1121
|
case "int":
|
|
1014
1122
|
return "i64";
|
|
1015
1123
|
case "float":
|
|
1124
|
+
// a `"number"` port is a FLOAT (run_behavior: `typeName(number) === "float"`, expr-eval.ts §value):
|
|
1125
|
+
// a JS number is IEEE754 → f64. (An i64 value is declared `"int"`; `"number"` is the float channel.)
|
|
1126
|
+
case "number":
|
|
1016
1127
|
return "f64";
|
|
1017
1128
|
case "bool":
|
|
1018
1129
|
return "bool";
|
|
@@ -1024,6 +1135,13 @@ function inputPortRustType(schema, plan) {
|
|
|
1024
1135
|
return `Vec<${renderTypeRef(deriveTypeRef(et, plan))}>`;
|
|
1025
1136
|
return "Value";
|
|
1026
1137
|
}
|
|
1138
|
+
case "map": {
|
|
1139
|
+
// an input MAP port (dynamic string keys, declared value elemType) lowers to a native BTreeMap.
|
|
1140
|
+
const et = schema.elemType;
|
|
1141
|
+
if (et !== undefined && plan !== undefined)
|
|
1142
|
+
return `std::collections::BTreeMap<String, ${renderTypeRef(deriveTypeRef(et, plan))}>`;
|
|
1143
|
+
return "Value";
|
|
1144
|
+
}
|
|
1027
1145
|
default:
|
|
1028
1146
|
return "Value";
|
|
1029
1147
|
}
|
|
@@ -1032,10 +1150,12 @@ function inputPortRustType(schema, plan) {
|
|
|
1032
1150
|
function inputScalarKind(schema) {
|
|
1033
1151
|
switch (schema?.type) {
|
|
1034
1152
|
case "string":
|
|
1153
|
+
case "literal": // a `"literal"` union is a constrained String (see inputPortRustType).
|
|
1035
1154
|
return "String";
|
|
1036
1155
|
case "int":
|
|
1037
1156
|
return "i64";
|
|
1038
1157
|
case "float":
|
|
1158
|
+
case "number": // a `"number"` port is the FLOAT channel (run_behavior: number → float / f64).
|
|
1039
1159
|
return "f64";
|
|
1040
1160
|
case "bool":
|
|
1041
1161
|
return "bool";
|
|
@@ -1132,8 +1252,16 @@ function emitNativeScalar(node, want, resolveRef) {
|
|
|
1132
1252
|
* a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
|
|
1133
1253
|
* ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
|
|
1134
1254
|
*/
|
|
1135
|
-
function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan, typedNodes) {
|
|
1255
|
+
function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan, typedNodes, asBinding, elemBaseExpr) {
|
|
1136
1256
|
const field = portFieldName(pn);
|
|
1257
|
+
// #108-map: a port that is a ref to a `$as` element MAP/NAMED field (or an input map port) → CLONE the
|
|
1258
|
+
// typed composite value directly off the element/input (native BTreeMap / struct — ZERO boxed Value).
|
|
1259
|
+
// Reuses portCompositeRef for the type; the value is the typed field access `.clone()`.
|
|
1260
|
+
if (plan !== undefined) {
|
|
1261
|
+
const compExpr = emitCompositePortValue(expr, inputPorts ?? {}, asBinding, plan, elemBaseExpr);
|
|
1262
|
+
if (compExpr !== null)
|
|
1263
|
+
return `${field}: ${compExpr}`;
|
|
1264
|
+
}
|
|
1137
1265
|
// #110: a componentRef port bound to a runtime array input port (with elemType) → the native Vec<ElemT>,
|
|
1138
1266
|
// cloned out of the concrete input struct (`in_.<field>.clone()`) — ZERO boxed Value on the read hot path.
|
|
1139
1267
|
if (inputPorts !== undefined && portArrayElemRef(expr, inputPorts, plan) !== null) {
|
|
@@ -1233,9 +1361,11 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1233
1361
|
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.`);
|
|
1234
1362
|
}
|
|
1235
1363
|
const typedNodes = new Map();
|
|
1364
|
+
// #125: control-gate は typeless(typedNodes に載せない)。map ノードは produced-array ref に正規化
|
|
1365
|
+
// (recorder SoT の element outType を {arr:element} へラップ / legacy {arr} はそのまま)。
|
|
1236
1366
|
for (const n of comp.body)
|
|
1237
1367
|
if (!isControlGate(n))
|
|
1238
|
-
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1368
|
+
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
|
|
1239
1369
|
const idToIndex = new Map();
|
|
1240
1370
|
comp.body.forEach((n, i) => idToIndex.set(n.id, i));
|
|
1241
1371
|
const opsLiteral = comp.body.map((n) => {
|
|
@@ -1471,17 +1601,41 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1471
1601
|
const ids = stage.map((i) => comp.body[i].id);
|
|
1472
1602
|
lines.push(` // ── bc#87 real-concurrency stage {${ids.map((x) => rustStrLit(x)).join(", ")}} — static parallel`);
|
|
1473
1603
|
lines.push(` // orchestration (scoped threads, bound=${concurrency}); preflight+commit in ascending index order.`);
|
|
1604
|
+
// #125/#108: a stage member may be a cond node — a pure Expression (NO handler/dispatch). The scoped-
|
|
1605
|
+
// thread orchestration governs only the componentRef fan-out members; a cond is settled in ascending
|
|
1606
|
+
// preflight order — BEFORE any dispatch — via the SAME emitCondArm the sequential path uses (no forked
|
|
1607
|
+
// lowering). emitCondArm routes internally: a when(...) control-gate cond (#136, isControlGate, no
|
|
1608
|
+
// outType) commits produced_<gate> ({ok}/null skip marker); a Φ-merge VALUE cond (#108, has outType,
|
|
1609
|
+
// then/else are data branches) materializes its typed result cell (the sibling Φ-join — e.g. two
|
|
1610
|
+
// when-gated connection relations each folding to `ref | emptyConn`). Both are mutually independent
|
|
1611
|
+
// (§1), so ascending-at-head is byte-equal to run_behavior. A cond never shares a stage with a node
|
|
1612
|
+
// that depends on it (an intra-stage parent/child dep is rejected by concurrencyPlan).
|
|
1613
|
+
const condMembers = stage.filter((i) => "cond" in comp.body[i]);
|
|
1614
|
+
const refMembers = stage.filter((i) => !("cond" in comp.body[i]));
|
|
1615
|
+
for (const i of condMembers) {
|
|
1616
|
+
lines.push(emitCondArm(comp, comp.body[i], atPos + stage.indexOf(i), typedNodes, plan, priorNodeAt));
|
|
1617
|
+
}
|
|
1618
|
+
// A cond-ONLY stage carries no handler fan-out — the conds above ARE the whole stage. Skip the scoped-
|
|
1619
|
+
// thread orchestration block entirely (an empty one is dead scaffolding clippy rejects: never_loop /
|
|
1620
|
+
// match_single_binding on a zero-job scope).
|
|
1621
|
+
if (refMembers.length === 0)
|
|
1622
|
+
return lines.join("\n");
|
|
1474
1623
|
lines.push(` {`);
|
|
1475
1624
|
// 1) PREFLIGHT (ascending): build each member's CONCRETE ports struct into a stage-scoped Option
|
|
1476
1625
|
// local. #114: a bindField relation child ALSO reads its produced-aware bound key from the SETTLED
|
|
1477
1626
|
// parent struct field (typed access — no serialize) into a stage-scoped Option; a None bound =>
|
|
1478
1627
|
// UNPRODUCED/skip (ports_<s> stays None), exactly run_behavior's preflightOp bindField skip, before
|
|
1479
1628
|
// any dispatch. The bound is captured (cloned) by the member's scoped-thread worker.
|
|
1480
|
-
for (const i of
|
|
1629
|
+
for (const i of refMembers) {
|
|
1481
1630
|
const node = comp.body[i];
|
|
1482
1631
|
const s = sanitize(node.id);
|
|
1483
1632
|
const structName = portsStructName(comp.name, node.id);
|
|
1484
1633
|
const gated = node.parent !== undefined && priorNodeAt(node.parent, atPos);
|
|
1634
|
+
// #125: a member gated by a PRIOR-stage control gate (`parent=gate, bindField:"ok"`). Its bindField is
|
|
1635
|
+
// the gate's skip sentinel, NOT a relation data key — the gate is typeless (no struct field to read).
|
|
1636
|
+
// Mirror the sequential arm: past the produced_<gate> gate we are past the skip, so the bound is the
|
|
1637
|
+
// non-None "ok" sentinel (the child's ports read the REAL data parent, not the gate).
|
|
1638
|
+
const parentIsGate = node.parent !== undefined && isControlGate(comp.body.find((b) => b.id === node.parent));
|
|
1485
1639
|
let ind = " ";
|
|
1486
1640
|
const closers = [];
|
|
1487
1641
|
if (node.bindField !== undefined) {
|
|
@@ -1493,7 +1647,10 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1493
1647
|
lines.push(`${ind}if produced_${sanitize(node.parent)}.get() {`);
|
|
1494
1648
|
closers.unshift(`${ind}}`);
|
|
1495
1649
|
ind += " ";
|
|
1496
|
-
if (node.bindField !== undefined) {
|
|
1650
|
+
if (node.bindField !== undefined && parentIsGate) {
|
|
1651
|
+
lines.push(`${ind}bound_${s} = Some("ok".to_string());`);
|
|
1652
|
+
}
|
|
1653
|
+
else if (node.bindField !== undefined) {
|
|
1497
1654
|
const parentRef = typedNodes.get(node.parent);
|
|
1498
1655
|
const { expr: bfExpr, ref: bfRef } = typedFieldAccess(`${typedCell(node.parent)}.borrow()`, parentRef, [node.bindField], plan);
|
|
1499
1656
|
if (bfRef.kind === "opt") {
|
|
@@ -1526,16 +1683,18 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1526
1683
|
}
|
|
1527
1684
|
// 2) DISPATCH (bounded scoped threads). Share `&H` (concrete trait; H: Sync). Each worker calls the
|
|
1528
1685
|
// concrete node_* method for its member's CONCRETE ports and stores the concrete row in its slot.
|
|
1529
|
-
|
|
1686
|
+
// #125: the fan-out (slots / jobs / workers) spans the COMPONENTREF members only — a control gate
|
|
1687
|
+
// dispatches no handler (committed in preflight above), so it is not a scoped-thread job.
|
|
1688
|
+
const nMembers = refMembers.length;
|
|
1530
1689
|
const boundN = Math.min(concurrency, nMembers);
|
|
1531
1690
|
const jobsVar = `jobs_${sanitize(comp.name)}`;
|
|
1532
|
-
for (const i of
|
|
1691
|
+
for (const i of refMembers) {
|
|
1533
1692
|
const s = sanitize(comp.body[i].id);
|
|
1534
1693
|
const rowT = rawRowStructName(comp.name, comp.body[i].id);
|
|
1535
1694
|
lines.push(` let slot_${s}: std::sync::Mutex<Option<Option<${rowT}>>> = std::sync::Mutex::new(None);`);
|
|
1536
1695
|
}
|
|
1537
1696
|
lines.push(` let mut ${jobsVar}: Vec<usize> = Vec::new();`);
|
|
1538
|
-
|
|
1697
|
+
refMembers.forEach((i, slot) => {
|
|
1539
1698
|
const s = sanitize(comp.body[i].id);
|
|
1540
1699
|
lines.push(` if ports_${s}.is_some() { ${jobsVar}.push(${slot}); }`);
|
|
1541
1700
|
});
|
|
@@ -1548,7 +1707,7 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1548
1707
|
lines.push(` if j >= ${jobsVar}.len() { break; }`);
|
|
1549
1708
|
lines.push(` let slot = ${jobsVar}[j];`);
|
|
1550
1709
|
lines.push(` match slot {`);
|
|
1551
|
-
|
|
1710
|
+
refMembers.forEach((i, slot) => {
|
|
1552
1711
|
const node = comp.body[i];
|
|
1553
1712
|
const s = sanitize(node.id);
|
|
1554
1713
|
const boundArg = node.bindField !== undefined ? `bound_${s}.clone()` : "None";
|
|
@@ -1564,7 +1723,7 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1564
1723
|
lines.push(` }`);
|
|
1565
1724
|
lines.push(` });`);
|
|
1566
1725
|
// 3) COMMIT (ascending index): interpret + move the concrete row into the outType cell.
|
|
1567
|
-
|
|
1726
|
+
refMembers.forEach((i) => {
|
|
1568
1727
|
const node = comp.body[i];
|
|
1569
1728
|
const meta = metas[i];
|
|
1570
1729
|
const s = sanitize(node.id);
|
|
@@ -1616,7 +1775,7 @@ isAsync = false) {
|
|
|
1616
1775
|
const batch = `${structName}Batch`;
|
|
1617
1776
|
// #108: over element type + the OWNED over-Vec expression (prior-node arr OR input array w/ elemType).
|
|
1618
1777
|
const { elemRef: overElemRef, overExpr } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
1619
|
-
const elemMat = mapElemMaterializerName(node.id);
|
|
1778
|
+
const elemMat = mapElemMaterializerName(comp.name, node.id);
|
|
1620
1779
|
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1621
1780
|
const asName = m.as;
|
|
1622
1781
|
const portNames = Object.keys(m.ports);
|
|
@@ -1676,7 +1835,8 @@ isAsync = false) {
|
|
|
1676
1835
|
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1677
1836
|
for (const pn of portNames) {
|
|
1678
1837
|
const ty = portFieldRustType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding, plan);
|
|
1679
|
-
|
|
1838
|
+
// #108-map: pass the element binding + base expr so a `$as` MAP/NAMED field port clones the typed composite.
|
|
1839
|
+
inits.push(emitPortInit(pn, m.ports[pn], ty, resolveRef, comp.inputPorts, plan, undefined, asBinding, `oel_${s}`));
|
|
1680
1840
|
}
|
|
1681
1841
|
out.push(`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
1682
1842
|
return out;
|
|
@@ -2135,7 +2295,7 @@ ${emitStructSurface([])}
|
|
|
2135
2295
|
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
2136
2296
|
for (const n of c.body)
|
|
2137
2297
|
if (!isControlGate(n))
|
|
2138
|
-
typedNodes.set(n.id, deriveTypeRef(n.outType, plan)); // #125: control-gate は
|
|
2298
|
+
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan)); // #125: control-gate typeless; map は produced-array ref に正規化
|
|
2139
2299
|
for (const n of c.body) {
|
|
2140
2300
|
if ("fanout" in n)
|
|
2141
2301
|
structs.push(emitFanoutPortsStructs(c, n, typedNodes, plan));
|
|
@@ -2234,7 +2394,7 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2234
2394
|
const typedNodes = new Map();
|
|
2235
2395
|
for (const n of c.body)
|
|
2236
2396
|
if (!isControlGate(n))
|
|
2237
|
-
typedNodes.set(n.id, deriveTypeRef(n.outType, plan)); // #125: control-gate は
|
|
2397
|
+
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan)); // #125: control-gate typeless; map は produced-array ref に正規化
|
|
2238
2398
|
const methods = [];
|
|
2239
2399
|
for (const n of c.body) {
|
|
2240
2400
|
if ("cond" in n)
|
|
@@ -2516,6 +2676,12 @@ function emitInDecoder(comp, plan) {
|
|
|
2516
2676
|
lines.push(` other => return Err(marshal_type_error(${rustStrLit(`decode input ${p}: expected array, got `)}, other.type_name())),`);
|
|
2517
2677
|
lines.push(` }`);
|
|
2518
2678
|
}
|
|
2679
|
+
else if (schema?.type === "map" && et !== undefined) {
|
|
2680
|
+
// #108-map: decode the boxed Value::Obj into the native BTreeMap<String, V> (test glue — the input
|
|
2681
|
+
// map port carries a declared elemType). materializeExpr covers the map de-box (obj → BTreeMap).
|
|
2682
|
+
const mapRef = { kind: "map", value: deriveTypeRef(et, plan) };
|
|
2683
|
+
lines.push(` in_.${field} = ${materializeExpr("v", mapRef, plan)};`);
|
|
2684
|
+
}
|
|
2519
2685
|
else if (scalar === undefined) {
|
|
2520
2686
|
lines.push(` in_.${field} = v.clone();`);
|
|
2521
2687
|
}
|