behavior-contracts 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/behavior.d.ts +7 -0
- package/dist/behavior.d.ts.map +1 -1
- package/dist/behavior.js.map +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +10 -3
- package/dist/builder.js.map +1 -1
- package/dist/generator/async-plan.d.ts +78 -0
- package/dist/generator/async-plan.d.ts.map +1 -0
- package/dist/generator/async-plan.js +116 -0
- package/dist/generator/async-plan.js.map +1 -0
- package/dist/generator/core.d.ts +30 -9
- package/dist/generator/core.d.ts.map +1 -1
- package/dist/generator/core.js.map +1 -1
- package/dist/generator/{emit-straightline-typed-go.d.ts → emit-shared-go-typed.d.ts} +11 -17
- package/dist/generator/emit-shared-go-typed.d.ts.map +1 -0
- package/dist/generator/emit-shared-go-typed.js +572 -0
- package/dist/generator/emit-shared-go-typed.js.map +1 -0
- package/dist/generator/emit-shared-go.d.ts +29 -0
- package/dist/generator/emit-shared-go.d.ts.map +1 -0
- package/dist/generator/{emit-straightline-go.js → emit-shared-go.js} +2 -34
- package/dist/generator/emit-shared-go.js.map +1 -0
- package/dist/generator/{emit-straightline-typed-rust.d.ts → emit-shared-rust-typed.d.ts} +11 -17
- package/dist/generator/emit-shared-rust-typed.d.ts.map +1 -0
- package/dist/generator/emit-shared-rust-typed.js +576 -0
- package/dist/generator/emit-shared-rust-typed.js.map +1 -0
- package/dist/generator/{emit-straightline-rust.d.ts → emit-shared-rust.d.ts} +1 -36
- package/dist/generator/emit-shared-rust.d.ts.map +1 -0
- package/dist/generator/{emit-straightline-rust.js → emit-shared-rust.js} +2 -16
- package/dist/generator/emit-shared-rust.js.map +1 -0
- package/dist/generator/emit-shared-typescript.d.ts +12 -0
- package/dist/generator/emit-shared-typescript.d.ts.map +1 -0
- package/dist/generator/{emit-straightline-typescript.js → emit-shared-typescript.js} +5 -20
- package/dist/generator/emit-shared-typescript.js.map +1 -0
- package/dist/generator/emit-straightline-typed-typescript.d.ts +1 -1
- package/dist/generator/emit-straightline-typed-typescript.d.ts.map +1 -1
- package/dist/generator/emit-straightline-typed-typescript.js +1 -1
- 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 +546 -82
- package/dist/generator/emit-typed-native-go.js.map +1 -1
- package/dist/generator/emit-typed-native-rust.d.ts +2 -1
- package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-rust.js +597 -110
- package/dist/generator/emit-typed-native-rust.js.map +1 -1
- package/dist/generator/index.d.ts +2 -12
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +10 -62
- package/dist/generator/index.js.map +1 -1
- package/dist/generator/native-expr.d.ts +120 -0
- package/dist/generator/native-expr.d.ts.map +1 -0
- package/dist/generator/native-expr.js +456 -0
- package/dist/generator/native-expr.js.map +1 -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.js +14 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plan.d.ts +6 -2
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +4 -0
- package/dist/plan.js.map +1 -1
- package/package.json +1 -1
- package/dist/generator/emit-go.d.ts +0 -19
- package/dist/generator/emit-go.d.ts.map +0 -1
- package/dist/generator/emit-go.js +0 -79
- package/dist/generator/emit-go.js.map +0 -1
- package/dist/generator/emit-raw-abi-go.d.ts +0 -45
- package/dist/generator/emit-raw-abi-go.d.ts.map +0 -1
- package/dist/generator/emit-raw-abi-go.js +0 -346
- package/dist/generator/emit-raw-abi-go.js.map +0 -1
- package/dist/generator/emit-raw-abi-rust.d.ts +0 -45
- package/dist/generator/emit-raw-abi-rust.d.ts.map +0 -1
- package/dist/generator/emit-raw-abi-rust.js +0 -380
- package/dist/generator/emit-raw-abi-rust.js.map +0 -1
- package/dist/generator/emit-rust.d.ts +0 -25
- package/dist/generator/emit-rust.d.ts.map +0 -1
- package/dist/generator/emit-rust.js +0 -125
- package/dist/generator/emit-rust.js.map +0 -1
- package/dist/generator/emit-straightline-go.d.ts +0 -60
- package/dist/generator/emit-straightline-go.d.ts.map +0 -1
- package/dist/generator/emit-straightline-go.js.map +0 -1
- package/dist/generator/emit-straightline-native-go.d.ts +0 -54
- package/dist/generator/emit-straightline-native-go.d.ts.map +0 -1
- package/dist/generator/emit-straightline-native-go.js +0 -835
- package/dist/generator/emit-straightline-native-go.js.map +0 -1
- package/dist/generator/emit-straightline-native-rust.d.ts +0 -27
- package/dist/generator/emit-straightline-native-rust.d.ts.map +0 -1
- package/dist/generator/emit-straightline-native-rust.js +0 -624
- package/dist/generator/emit-straightline-native-rust.js.map +0 -1
- package/dist/generator/emit-straightline-php.d.ts +0 -46
- package/dist/generator/emit-straightline-php.d.ts.map +0 -1
- package/dist/generator/emit-straightline-php.js +0 -941
- package/dist/generator/emit-straightline-php.js.map +0 -1
- package/dist/generator/emit-straightline-python.d.ts +0 -36
- package/dist/generator/emit-straightline-python.d.ts.map +0 -1
- package/dist/generator/emit-straightline-python.js +0 -855
- package/dist/generator/emit-straightline-python.js.map +0 -1
- package/dist/generator/emit-straightline-rust.d.ts.map +0 -1
- package/dist/generator/emit-straightline-rust.js.map +0 -1
- package/dist/generator/emit-straightline-typed-go.d.ts.map +0 -1
- package/dist/generator/emit-straightline-typed-go.js +0 -1114
- package/dist/generator/emit-straightline-typed-go.js.map +0 -1
- package/dist/generator/emit-straightline-typed-rust.d.ts.map +0 -1
- package/dist/generator/emit-straightline-typed-rust.js +0 -1155
- package/dist/generator/emit-straightline-typed-rust.js.map +0 -1
- package/dist/generator/emit-straightline-typescript.d.ts +0 -47
- package/dist/generator/emit-straightline-typescript.d.ts.map +0 -1
- package/dist/generator/emit-straightline-typescript.js.map +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { GeneratorFailure } from "./core.js";
|
|
2
|
-
import { rustStraightlineInternals } from "./emit-
|
|
3
|
-
import { rustTypedInternals } from "./emit-
|
|
2
|
+
import { rustStraightlineInternals } from "./emit-shared-rust.js";
|
|
3
|
+
import { rustTypedInternals } from "./emit-shared-rust-typed.js";
|
|
4
4
|
import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
|
|
5
5
|
import { concurrencyPlan, execPlan, analyzeSequentialOps, classifyExpr } from "./straightline.js";
|
|
6
|
+
import { NativeExprCompiler } from "./native-expr.js";
|
|
7
|
+
import { buildAsyncPlans } from "./async-plan.js";
|
|
6
8
|
const { sanitize, rustStrLit } = rustStraightlineInternals;
|
|
7
9
|
const { rustFieldName, renderTypeRef, typedCell, typedFieldAccess, serializeTyped, emitTypeDecls, emitDefaults, emitSerializers, emitTypedValue, emitMarshallers, emitMarshalHelpers, materializeExpr, } = rustTypedInternals;
|
|
8
10
|
/** rustErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
|
|
@@ -84,7 +86,32 @@ function staticArrElems(node) {
|
|
|
84
86
|
const arg = node.arr;
|
|
85
87
|
return Array.isArray(arg) ? arg : null;
|
|
86
88
|
}
|
|
87
|
-
|
|
89
|
+
/**
|
|
90
|
+
* portArrayElemRef (#110) — resolve a componentRef input port that binds a RUNTIME ARRAY (an IN-list /
|
|
91
|
+
* array-bound WHERE head) to its native element TypeRef, or null when not a covered array. The covered
|
|
92
|
+
* shape is a SINGLE-SEGMENT, non-opt `ref` into an INPUT ARRAY port carrying a resolvable `elemType`
|
|
93
|
+
* (PortSchema.elemType — BC does NOT infer, consumer-interface C3). The port lowers to a native
|
|
94
|
+
* `Vec<ElemT>` fed from `in_.<field>.clone()`. Reuses the bc#108 elemType lowering, extended from the
|
|
95
|
+
* `map…over` element-source path to a plain componentRef port. A static string array is a different
|
|
96
|
+
* covered shape (Vec<&'static str>) — handled ahead of this. No resolvable elemType / opt / multi-segment
|
|
97
|
+
* ref / non-array schema / no plan → null (caller keeps its existing fail-closed behaviour). */
|
|
98
|
+
function portArrayElemRef(node, inputPorts, plan) {
|
|
99
|
+
if (plan === undefined)
|
|
100
|
+
return null;
|
|
101
|
+
if (staticStringArrElems(node) !== null)
|
|
102
|
+
return null;
|
|
103
|
+
const e = classifyExpr(node);
|
|
104
|
+
if (e.kind !== "ref" || e.opt || e.path.length !== 1)
|
|
105
|
+
return null;
|
|
106
|
+
const schema = inputPorts?.[e.path[0]];
|
|
107
|
+
if (schema === undefined || (schema.type !== "array" && schema.type !== "arr"))
|
|
108
|
+
return null;
|
|
109
|
+
const et = schema.elemType;
|
|
110
|
+
if (et === undefined)
|
|
111
|
+
return null;
|
|
112
|
+
return deriveTypeRef(et, plan);
|
|
113
|
+
}
|
|
114
|
+
function inferPortType(node, inputPorts, asBinding) {
|
|
88
115
|
if (staticArrElems(node) !== null)
|
|
89
116
|
return "Value";
|
|
90
117
|
const e = classifyExpr(node);
|
|
@@ -95,6 +122,18 @@ function inferPortType(node, inputPorts) {
|
|
|
95
122
|
case "bool":
|
|
96
123
|
return "bool";
|
|
97
124
|
case "ref": {
|
|
125
|
+
// #108: a `$as`-headed ref (map element binding) — resolve the element field's scalar type.
|
|
126
|
+
if (asBinding !== undefined && e.path[0] === asBinding.name) {
|
|
127
|
+
try {
|
|
128
|
+
const { ref } = typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan);
|
|
129
|
+
if (ref.kind === "scalar" && ref.scalar !== "null")
|
|
130
|
+
return rustScalar(ref.scalar);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return "Value";
|
|
134
|
+
}
|
|
135
|
+
return "Value";
|
|
136
|
+
}
|
|
98
137
|
if (e.path.length === 1) {
|
|
99
138
|
const s = inputPorts[e.path[0]];
|
|
100
139
|
if (s) {
|
|
@@ -139,13 +178,17 @@ function staticStringArrElems(node) {
|
|
|
139
178
|
* Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no `Value`
|
|
140
179
|
* fallback on the native module). `where` names the emission site for the error.
|
|
141
180
|
*/
|
|
142
|
-
function portFieldRustType(node, inputPorts, where = "port") {
|
|
181
|
+
function portFieldRustType(node, inputPorts, where = "port", asBinding, plan) {
|
|
143
182
|
if (staticStringArrElems(node) !== null)
|
|
144
183
|
return "Vec<&'static str>";
|
|
184
|
+
// #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → Vec<ElemT>.
|
|
185
|
+
const arrElem = portArrayElemRef(node, inputPorts, plan);
|
|
186
|
+
if (arrElem !== null)
|
|
187
|
+
return `Vec<${renderTypeRef(arrElem)}>`;
|
|
145
188
|
const num = numLiteralRustExpr(node);
|
|
146
189
|
if (num !== null)
|
|
147
190
|
return typeof node === "number" && Number.isInteger(node) ? "i64" : "f64";
|
|
148
|
-
const ft = inferPortType(node, inputPorts);
|
|
191
|
+
const ft = inferPortType(node, inputPorts, asBinding);
|
|
149
192
|
if (ft === "Value") {
|
|
150
193
|
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.`);
|
|
151
194
|
}
|
|
@@ -158,13 +201,13 @@ function portFieldRustType(node, inputPorts, where = "port") {
|
|
|
158
201
|
* reads `ports.f_<field>` DIRECTLY. The struct derives Clone (a bc#87 parallel stage clones it into the
|
|
159
202
|
* per-worker dispatch). Every field is a NATIVE Rust type (bc#90 / runtime-free) — NO boxed `Value`.
|
|
160
203
|
*/
|
|
161
|
-
function emitPortsStruct(comp, node) {
|
|
204
|
+
function emitPortsStruct(comp, node, asBinding, plan) {
|
|
162
205
|
const name = portsStructName(comp.name, node.id);
|
|
163
206
|
const portNames = Object.keys(node.ports);
|
|
164
207
|
if (portNames.length === 0) {
|
|
165
208
|
return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
|
|
166
209
|
}
|
|
167
|
-
const types = portNames.map((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'
|
|
210
|
+
const types = portNames.map((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding, plan));
|
|
168
211
|
const fields = portNames
|
|
169
212
|
.map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
|
|
170
213
|
.join("\n");
|
|
@@ -181,11 +224,14 @@ ${fields}
|
|
|
181
224
|
* struct carries the Vec of per-element CONCRETE ports structs (the handler reads them off the concrete
|
|
182
225
|
* struct — no by-name accessor, no key-value object).
|
|
183
226
|
*/
|
|
184
|
-
function emitMapPortsStructs(comp, node) {
|
|
227
|
+
function emitMapPortsStructs(comp, node, typedNodes, plan) {
|
|
185
228
|
const m = node.map;
|
|
186
229
|
const name = portsStructName(comp.name, node.id);
|
|
187
230
|
const batch = `${name}Batch`;
|
|
188
|
-
|
|
231
|
+
// #108: element ports may read the `$as` binding — resolve the over element type for native typing.
|
|
232
|
+
const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
233
|
+
const asBinding = { name: m.as, ref: elemRef, plan };
|
|
234
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan);
|
|
189
235
|
return `${elemStruct}
|
|
190
236
|
|
|
191
237
|
// ${batch} — CONCRETE batched ports for map '${node.id}': the Vec of per-element CONCRETE ports structs
|
|
@@ -196,6 +242,188 @@ pub struct ${batch} {
|
|
|
196
242
|
pub items: Vec<${name}>,
|
|
197
243
|
}`;
|
|
198
244
|
}
|
|
245
|
+
// ── native expression compiler backend (#108) — cond `if` / map `when` / cond branches (Rust) ────────
|
|
246
|
+
//
|
|
247
|
+
// The runtime-free native expression compiler (native-expr.ts) mirrors run_behavior's cond/guard with
|
|
248
|
+
// BYTE-EQUAL semantics. The value-dependent runtime failures (INT_OVERFLOW / NAN_OR_INF / MOD_ZERO /
|
|
249
|
+
// PRECISION_LOSS) are emitted as calls to a FIXED set of RUNTIME-FREE helper fns baked into the module
|
|
250
|
+
// (bc_expr_* below) returning Result<T, BehaviorError> — the covered module still imports ZERO bc runtime.
|
|
251
|
+
// The runner is Result<_, BehaviorError>, so a fallible helper propagates with `?`.
|
|
252
|
+
let rustExprTempSeq = 0;
|
|
253
|
+
/** module-level accumulator: set when the native-expr compiler emits a fallible helper call (so emit()
|
|
254
|
+
* bakes the helper library). Reset at the start of each emit(). */
|
|
255
|
+
const rustExprUsed = { helpers: false };
|
|
256
|
+
/** makeRustExprBackend — renders native-expr for Rust. `resolveHead` maps a ref head to an OWNED typed
|
|
257
|
+
* base expr (a prior-node cell clone / an input field / a $as element). Sets rustExprUsed.helpers when
|
|
258
|
+
* a fallible helper is emitted. The runner is Result<_, BehaviorError>, so hoistFallible uses `?`. */
|
|
259
|
+
function makeRustExprBackend(resolveHead, plan) {
|
|
260
|
+
const used = rustExprUsed;
|
|
261
|
+
return {
|
|
262
|
+
lang: "rust",
|
|
263
|
+
freshTemp: () => `ex_${rustExprTempSeq++}`,
|
|
264
|
+
renderType: (ref) => renderTypeRef(ref),
|
|
265
|
+
scalarType: (s) => rustScalar(s),
|
|
266
|
+
strLit: (s) => `${rustStrLit(s)}.to_string()`,
|
|
267
|
+
boolLit: (b) => (b ? "true" : "false"),
|
|
268
|
+
intLit: (dec) => `${dec}i64`,
|
|
269
|
+
floatLit: (n) => `${rustFloatLitExpr(n)}f64`,
|
|
270
|
+
fieldAccess: (baseExpr, baseRef, path) => typedFieldAccess(baseExpr, baseRef, path, plan),
|
|
271
|
+
resolveHead,
|
|
272
|
+
hoistFallible: (temp, _ty, call) => {
|
|
273
|
+
used.helpers = true;
|
|
274
|
+
return [`let ${temp} = ${call}?;`];
|
|
275
|
+
},
|
|
276
|
+
helperCall: (helper, args) => {
|
|
277
|
+
used.helpers = true;
|
|
278
|
+
// the string code-point compare helpers take &str — borrow the owned String args.
|
|
279
|
+
const isStrCmp = helper === "strLt" || helper === "strLe" || helper === "strGt" || helper === "strGe";
|
|
280
|
+
const passed = isStrCmp ? args.map((a) => `&${a}`) : args;
|
|
281
|
+
// lenArr takes &[T] — borrow the owned Vec.
|
|
282
|
+
const finalArgs = helper === "lenArr" ? args.map((a) => `&${a}`) : passed;
|
|
283
|
+
return `bc_expr_${rustSnake(helper)}(${finalArgs.join(", ")})`;
|
|
284
|
+
},
|
|
285
|
+
binOp: (op, a, b) => `(${a} ${op} ${b})`,
|
|
286
|
+
concat: (parts) => {
|
|
287
|
+
// native String concat via a `[&str; N].concat()` — a string-literal part (`"…".to_string()`) is
|
|
288
|
+
// used as the bare `"…"` &str; any other owned String is borrowed with `.as_str()`. This shape is
|
|
289
|
+
// clippy-clean (no to_string_in_format_args / single_char_push_str / unnecessary_to_string).
|
|
290
|
+
const refs = parts.map((p) => {
|
|
291
|
+
const m = /^(".*")\.to_string\(\)$/.exec(p);
|
|
292
|
+
return m ? m[1] : `(${p}).as_str()`;
|
|
293
|
+
});
|
|
294
|
+
return `[${refs.join(", ")}].concat()`;
|
|
295
|
+
},
|
|
296
|
+
notOp: (a) => `(!${a})`,
|
|
297
|
+
structLit: (name, inits) => `${name} { ${inits.map((i) => `${rustFieldName(i.field)}: ${i.expr}`).join(", ")} }`,
|
|
298
|
+
arrLit: (elemTy, items) => `{ let __v: Vec<${elemTy}> = vec![${items.join(", ")}]; __v }`,
|
|
299
|
+
ternary: (cond, t, e, _ty) => `(if ${rustStripOuterParens(cond)} { ${rustStripOuterParens(t)} } else { ${rustStripOuterParens(e)} })`,
|
|
300
|
+
shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
|
|
301
|
+
// and: let temp = if left { <rStmts> rExpr } else { false }; or: if left { true } else { <rStmts> rExpr }.
|
|
302
|
+
// build a block that runs rStmts then yields rExpr (parens stripped — block-tail is not a sub-expr).
|
|
303
|
+
const rblock = `{ ${rStmts.join(" ")} ${rustStripOuterParens(rExpr)} }`;
|
|
304
|
+
if (op === "and") {
|
|
305
|
+
return [`let ${temp}: bool = if ${rustStripOuterParens(left)} ${rblock} else { false };`];
|
|
306
|
+
}
|
|
307
|
+
return [`let ${temp}: bool = if ${rustStripOuterParens(left)} { true } else ${rblock};`];
|
|
308
|
+
},
|
|
309
|
+
condGuard: (temp, ty, cond, tStmts, tExpr, eStmts, eExpr) => {
|
|
310
|
+
const tblock = `{ ${tStmts.join(" ")} ${rustStripOuterParens(tExpr)} }`;
|
|
311
|
+
const eblock = `{ ${eStmts.join(" ")} ${rustStripOuterParens(eExpr)} }`;
|
|
312
|
+
return [`let ${temp}: ${ty} = if ${rustStripOuterParens(cond)} ${tblock} else ${eblock};`];
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/** camelCase helper name → snake_case for the Rust helper fn (addI64 → add_i64, strLt → str_lt). */
|
|
317
|
+
function rustSnake(name) {
|
|
318
|
+
return name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").replace(/([0-9])([a-z])/g, "$1_$2").toLowerCase();
|
|
319
|
+
}
|
|
320
|
+
/** strip ONE redundant outer paren pair from an expr (clippy `if (cond)` unused_parens). Only when the
|
|
321
|
+
* whole string is a single balanced `( … )` — a `(a) && (b)` stays untouched. */
|
|
322
|
+
function rustStripOuterParens(expr) {
|
|
323
|
+
if (!expr.startsWith("(") || !expr.endsWith(")"))
|
|
324
|
+
return expr;
|
|
325
|
+
let depth = 0;
|
|
326
|
+
for (let i = 0; i < expr.length; i++) {
|
|
327
|
+
if (expr[i] === "(")
|
|
328
|
+
depth++;
|
|
329
|
+
else if (expr[i] === ")") {
|
|
330
|
+
depth--;
|
|
331
|
+
if (depth === 0 && i !== expr.length - 1)
|
|
332
|
+
return expr; // closed before the end → not a single wrap
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return expr.slice(1, -1);
|
|
336
|
+
}
|
|
337
|
+
/** Rust float literal (finite; integral gets `.0`). */
|
|
338
|
+
function rustFloatLitExpr(n) {
|
|
339
|
+
if (Number.isInteger(n))
|
|
340
|
+
return `${n}.0`;
|
|
341
|
+
return String(n);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* emitExprHelpersRust — the FIXED runtime-free native-expr helper library (Rust). Emitted once per
|
|
345
|
+
* covered module that uses cond/guard/arith. Each helper is BYTE-EQUAL to expr-eval.ts core. Failures
|
|
346
|
+
* are the LOCAL BehaviorError with the SAME code strings as run_behavior.
|
|
347
|
+
*/
|
|
348
|
+
function emitExprHelpersRust() {
|
|
349
|
+
return `// ── native-expr runtime-free helper library (#108) — byte-equal to expr-eval.ts core ──
|
|
350
|
+
#[allow(dead_code)]
|
|
351
|
+
fn bc_expr_add_i64(a: i64, b: i64) -> Result<i64, BehaviorError> {
|
|
352
|
+
a.checked_add(b).ok_or_else(|| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
|
|
353
|
+
}
|
|
354
|
+
#[allow(dead_code)]
|
|
355
|
+
fn bc_expr_sub_i64(a: i64, b: i64) -> Result<i64, BehaviorError> {
|
|
356
|
+
a.checked_sub(b).ok_or_else(|| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
|
|
357
|
+
}
|
|
358
|
+
#[allow(dead_code)]
|
|
359
|
+
fn bc_expr_mul_i64(a: i64, b: i64) -> Result<i64, BehaviorError> {
|
|
360
|
+
a.checked_mul(b).ok_or_else(|| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
|
|
361
|
+
}
|
|
362
|
+
#[allow(dead_code)]
|
|
363
|
+
fn bc_expr_neg_i64(a: i64) -> Result<i64, BehaviorError> {
|
|
364
|
+
a.checked_neg().ok_or_else(|| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
|
|
365
|
+
}
|
|
366
|
+
#[allow(dead_code)]
|
|
367
|
+
fn bc_expr_check_finite(v: f64) -> Result<f64, BehaviorError> {
|
|
368
|
+
if v.is_finite() { Ok(v) } else { Err(BehaviorError::new("NAN_OR_INF", "non-finite float")) }
|
|
369
|
+
}
|
|
370
|
+
#[allow(dead_code)]
|
|
371
|
+
fn bc_expr_add_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a + b) }
|
|
372
|
+
#[allow(dead_code)]
|
|
373
|
+
fn bc_expr_sub_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a - b) }
|
|
374
|
+
#[allow(dead_code)]
|
|
375
|
+
fn bc_expr_mul_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a * b) }
|
|
376
|
+
#[allow(dead_code)]
|
|
377
|
+
fn bc_expr_neg_f(a: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(-a) }
|
|
378
|
+
#[allow(dead_code)]
|
|
379
|
+
fn bc_expr_mod_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a % b) }
|
|
380
|
+
#[allow(dead_code)]
|
|
381
|
+
fn bc_expr_mod_i64(a: i64, b: i64) -> Result<i64, BehaviorError> {
|
|
382
|
+
if b == 0 { return Err(BehaviorError::new("MOD_ZERO", "int mod by zero")); }
|
|
383
|
+
// Rust % is truncated (sign of dividend) — matches modCore. MIN % -1 would panic; guard it.
|
|
384
|
+
if a == i64::MIN && b == -1 { return Ok(0); }
|
|
385
|
+
Ok(a % b)
|
|
386
|
+
}
|
|
387
|
+
#[allow(dead_code)]
|
|
388
|
+
fn bc_expr_widen_i64f(a: i64) -> Result<f64, BehaviorError> {
|
|
389
|
+
if !(-9007199254740992..=9007199254740992).contains(&a) {
|
|
390
|
+
return Err(BehaviorError::new("PRECISION_LOSS", "int exceeds exact float range (±2^53)"));
|
|
391
|
+
}
|
|
392
|
+
Ok(a as f64)
|
|
393
|
+
}
|
|
394
|
+
#[allow(dead_code)]
|
|
395
|
+
fn bc_expr_div_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a / b) }
|
|
396
|
+
#[allow(dead_code)]
|
|
397
|
+
fn bc_expr_len_arr<T>(a: &[T]) -> Result<i64, BehaviorError> {
|
|
398
|
+
i64::try_from(a.len()).map_err(|_| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
|
|
399
|
+
}
|
|
400
|
+
// code-point (char) order comparison — matches cmpCodePoints (char-by-char, NOT byte order).
|
|
401
|
+
#[allow(dead_code)]
|
|
402
|
+
fn bc_expr_cmp_cp(a: &str, b: &str) -> std::cmp::Ordering {
|
|
403
|
+
let mut ia = a.chars();
|
|
404
|
+
let mut ib = b.chars();
|
|
405
|
+
loop {
|
|
406
|
+
match (ia.next(), ib.next()) {
|
|
407
|
+
(None, None) => return std::cmp::Ordering::Equal,
|
|
408
|
+
(None, Some(_)) => return std::cmp::Ordering::Less,
|
|
409
|
+
(Some(_), None) => return std::cmp::Ordering::Greater,
|
|
410
|
+
(Some(ca), Some(cb)) => {
|
|
411
|
+
if ca != cb {
|
|
412
|
+
return if (ca as u32) < (cb as u32) { std::cmp::Ordering::Less } else { std::cmp::Ordering::Greater };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
#[allow(dead_code)]
|
|
419
|
+
fn bc_expr_str_lt(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) == std::cmp::Ordering::Less }
|
|
420
|
+
#[allow(dead_code)]
|
|
421
|
+
fn bc_expr_str_le(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) != std::cmp::Ordering::Greater }
|
|
422
|
+
#[allow(dead_code)]
|
|
423
|
+
fn bc_expr_str_gt(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) == std::cmp::Ordering::Greater }
|
|
424
|
+
#[allow(dead_code)]
|
|
425
|
+
fn bc_expr_str_ge(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) != std::cmp::Ordering::Less }`;
|
|
426
|
+
}
|
|
199
427
|
// ── CONCRETE per-node row structs + row→outType copiers (mirror the Go concrete contract) ──────────
|
|
200
428
|
//
|
|
201
429
|
// Each covered node has a CONCRETE row struct `RawRowNR<Comp><Node>` whose fields are the node's projected
|
|
@@ -209,6 +437,8 @@ pub struct ${batch} {
|
|
|
209
437
|
function emitRawRowStructs(comp, typedNodes, plan) {
|
|
210
438
|
const parts = [];
|
|
211
439
|
for (const n of comp.body) {
|
|
440
|
+
if ("cond" in n)
|
|
441
|
+
continue; // #108: a cond node has no handler-result row (pure Expression).
|
|
212
442
|
const ref = typedNodes.get(n.id);
|
|
213
443
|
if ("map" in n) {
|
|
214
444
|
const arrRef = ref;
|
|
@@ -291,12 +521,14 @@ function boundRustType(node, typedNodes, plan) {
|
|
|
291
521
|
* struct (per-element). This is the fully-concrete, runtime-free replacement for the NativeRawComponentExec
|
|
292
522
|
* seam.
|
|
293
523
|
*/
|
|
294
|
-
function emitHandlerTrait(comp, typedNodes, plan,
|
|
295
|
-
// bc#97: async I/O model
|
|
296
|
-
// async node_
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
|
|
524
|
+
function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
525
|
+
// bc#97 (increment 2): async I/O model is PER-TERMINAL-NODE. A node whose terminal handler is declared
|
|
526
|
+
// async gets an `async fn node_*` (the consumer's impl returns a future the runner awaits at the call
|
|
527
|
+
// site); a sync terminal stays plain `fn node_*` (called directly, no await). Mixed sync+async in one
|
|
528
|
+
// trait is fine. `async fn` needs NO external runtime to compile — the future is `async fn` sugar over
|
|
529
|
+
// std, so the covered module stays runtime-free either way. Uniform ioModel:'async' ⇒ every method
|
|
530
|
+
// async (byte-identical to increment-1); ioModel unset/'sync' ⇒ every method plain `fn` (pre-#97).
|
|
531
|
+
const fnKwFor = (nodeId) => (ap.nodeIsAsync(nodeId) ? "async fn" : "fn");
|
|
300
532
|
const lines = [];
|
|
301
533
|
lines.push(`// ${handlerTraitName(comp.name)} — the CONCRETE per-component handler seam: one typed method`);
|
|
302
534
|
lines.push(`// per covered node (native ports struct IN, concrete row struct OUT). No generic boxed-ports`);
|
|
@@ -304,7 +536,10 @@ function emitHandlerTrait(comp, typedNodes, plan, isAsync = false) {
|
|
|
304
536
|
lines.push(`// node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
|
|
305
537
|
lines.push(`pub trait ${handlerTraitName(comp.name)} {`);
|
|
306
538
|
for (const n of comp.body) {
|
|
539
|
+
if ("cond" in n)
|
|
540
|
+
continue; // #108: a cond node has no handler method (pure Expression).
|
|
307
541
|
const method = nodeMethodName(n.id);
|
|
542
|
+
const fnKw = fnKwFor(n.id);
|
|
308
543
|
const bt = boundRustType(n, typedNodes, plan);
|
|
309
544
|
if ("map" in n) {
|
|
310
545
|
const m = n.map;
|
|
@@ -368,8 +603,11 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
|
368
603
|
const elemRowGo = rawElemStructName(comp.name, n.id);
|
|
369
604
|
const intoRowRef = mapElemRowRef(n, arrRef, plan);
|
|
370
605
|
const inits = [];
|
|
371
|
-
|
|
372
|
-
|
|
606
|
+
// Copy scalars (i64/f64/bool) copy directly; String/named/arr/opt clone (clippy clone_on_copy clean).
|
|
607
|
+
for (const f of overFields) {
|
|
608
|
+
const isCopy = f.type.kind === "scalar" && (f.type.scalar === "int" || f.type.scalar === "float" || f.type.scalar === "bool");
|
|
609
|
+
inits.push(` ${rustFieldName(f.name)}: over.${rustFieldName(f.name)}${isCopy ? "" : ".clone()"},`);
|
|
610
|
+
}
|
|
373
611
|
// move the into field off the concrete element row (named → struct literal; scalar/arr/opt → .val).
|
|
374
612
|
const intoValue = intoRowRef.kind === "named"
|
|
375
613
|
? `${intoRowRef.name} { ${findDecl(plan, intoRowRef.name).fields.map((f) => `${rustFieldName(f.name)}: into.${rustFieldName(f.name)}`).join(", ")} }`
|
|
@@ -385,17 +623,33 @@ ${inits.join("\n")}
|
|
|
385
623
|
}
|
|
386
624
|
return parts.join("\n\n");
|
|
387
625
|
}
|
|
388
|
-
/**
|
|
389
|
-
|
|
626
|
+
/**
|
|
627
|
+
* mapOverElemInfo — resolve a covered map's `over` collection element TypeRef + the OWNED Rust
|
|
628
|
+
* expression that yields the over Vec (#108, rust twin): prior-node arr field (cell clone) or an input
|
|
629
|
+
* array port with elemType (`in_.<field>.clone()`). Fail-closed for an input array without elemType.
|
|
630
|
+
*/
|
|
631
|
+
function mapOverElemInfo(comp, node, typedNodes, plan) {
|
|
390
632
|
const m = node.map;
|
|
391
633
|
const e = classifyExpr(m.over);
|
|
392
634
|
if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
|
|
393
635
|
const baseRef = typedNodes.get(e.path[0]);
|
|
394
636
|
const { ref } = typedFieldAccess(`${typedCell(e.path[0])}.borrow()`, baseRef, e.path.slice(1), plan);
|
|
395
|
-
if (ref.kind === "arr")
|
|
396
|
-
|
|
637
|
+
if (ref.kind === "arr") {
|
|
638
|
+
const fieldPath = e.path.slice(1).map((p) => `.${rustFieldName(p)}`).join("");
|
|
639
|
+
return { elemRef: ref.elem, overExpr: `${typedCell(e.path[0])}.borrow()${fieldPath}.clone()` };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (e.kind === "ref" && e.path.length === 1) {
|
|
643
|
+
const schema = comp.inputPorts?.[e.path[0]];
|
|
644
|
+
const et = schema?.elemType;
|
|
645
|
+
if (et !== undefined)
|
|
646
|
+
return { elemRef: deriveTypeRef(et, plan), overExpr: `in_.${rustFieldName(e.path[0])}.clone()` };
|
|
397
647
|
}
|
|
398
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined map (#
|
|
648
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined map (#108): map '${node.id}' 'over' must be a ref to a prior node's typed arr field OR an input array port with a declared elemType. BC does not infer input-array element types (consumer-interface C3).`);
|
|
649
|
+
}
|
|
650
|
+
/** The Rust type of the over-element (back-compat shim). */
|
|
651
|
+
function mapOverElemType(comp, node, typedNodes, plan) {
|
|
652
|
+
return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
|
|
399
653
|
}
|
|
400
654
|
// ── eligibility ─────────────────────────────────────────────────────────────────────
|
|
401
655
|
function reconstructR(e) {
|
|
@@ -489,19 +743,30 @@ function outputIsNativeLowerable(comp) {
|
|
|
489
743
|
/**
|
|
490
744
|
* coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet), rust twin of the go predicate.
|
|
491
745
|
*/
|
|
492
|
-
function coveredMapNode(n, bodyIds) {
|
|
746
|
+
function coveredMapNode(n, bodyIds, comp) {
|
|
493
747
|
if (n === null || typeof n !== "object" || !("map" in n))
|
|
494
748
|
return false;
|
|
495
749
|
const m = n.map;
|
|
496
|
-
|
|
750
|
+
// #108 (rust twin of the go predicate): a guarded map (`when`) IS covered with NO `into` (guard+into
|
|
751
|
+
// is heterogeneous — fail-closed). over element type must be statically resolvable: prior-node arr
|
|
752
|
+
// field, or an input array port carrying `elemType` (BC does not infer — consumer-interface C3).
|
|
753
|
+
if (m.when !== undefined && m.into !== undefined)
|
|
497
754
|
return false;
|
|
498
755
|
if (m.policy !== undefined && m.policy !== "fail")
|
|
499
756
|
return false;
|
|
500
757
|
if (m.relationKind !== undefined && m.relationKind !== "single" && m.relationKind !== "connection")
|
|
501
758
|
return false;
|
|
502
759
|
const overE = classifyExpr(m.over);
|
|
503
|
-
if (overE.kind !== "ref"
|
|
760
|
+
if (overE.kind !== "ref")
|
|
504
761
|
return false;
|
|
762
|
+
const head = overE.path[0];
|
|
763
|
+
if (!bodyIds.has(head)) {
|
|
764
|
+
if (overE.path.length !== 1)
|
|
765
|
+
return false;
|
|
766
|
+
const schema = comp.inputPorts?.[head];
|
|
767
|
+
if (schema === undefined || schema.elemType === undefined)
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
505
770
|
const ports = m.ports;
|
|
506
771
|
for (const p of Object.keys(ports))
|
|
507
772
|
if (!portIsStatic(ports[p]))
|
|
@@ -533,11 +798,16 @@ function coverageRejectReason(comp) {
|
|
|
533
798
|
}
|
|
534
799
|
const bodyIds = new Set(comp.body.map((b) => b.id));
|
|
535
800
|
for (const n of comp.body) {
|
|
536
|
-
if ("cond" in n)
|
|
537
|
-
|
|
801
|
+
if ("cond" in n) {
|
|
802
|
+
// #108: a cond node is native-covered (its if/then/else compile via the runtime-free
|
|
803
|
+
// NativeExprCompiler; an uncovered expr fails closed at emit). cond requires outType.
|
|
804
|
+
if (n.outType === undefined)
|
|
805
|
+
return `node '${n.id}' cond has no outType (required to de-box the branch join)`;
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
538
808
|
if ("map" in n) {
|
|
539
|
-
if (!coveredMapNode(n, bodyIds))
|
|
540
|
-
return `node '${n.id}' is a non-covered map shape (
|
|
809
|
+
if (!coveredMapNode(n, bodyIds, comp))
|
|
810
|
+
return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
|
|
541
811
|
continue;
|
|
542
812
|
}
|
|
543
813
|
if (!("component" in n))
|
|
@@ -576,8 +846,8 @@ function assertTypedCoverage(comp) {
|
|
|
576
846
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#77): component '${comp.name}' is a sequential componentRef read but lacks outType/outputType on every node — the combined emitter de-boxes the RESULT into the node's outType struct, so a covered read MUST be fully typed (bc#45). Add outType annotations, or regenerate on rust-straightline-native (boxed result).`);
|
|
577
847
|
}
|
|
578
848
|
// ── typed input struct (InNR<Comp>) + NATIVE scalar port lowering ────────────────────────────
|
|
579
|
-
/** Rust type for an input port's declared type (scalar → concrete;
|
|
580
|
-
function inputPortRustType(schema) {
|
|
849
|
+
/** Rust type for an input port's declared type (scalar → concrete; array+elemType → Vec<ElemT>; else Value). */
|
|
850
|
+
function inputPortRustType(schema, plan) {
|
|
581
851
|
switch (schema?.type) {
|
|
582
852
|
case "string":
|
|
583
853
|
return "String";
|
|
@@ -587,6 +857,14 @@ function inputPortRustType(schema) {
|
|
|
587
857
|
return "f64";
|
|
588
858
|
case "bool":
|
|
589
859
|
return "bool";
|
|
860
|
+
case "array":
|
|
861
|
+
case "arr": {
|
|
862
|
+
// #108: an input ARRAY port with a declared elemType lowers to a native Vec<ElemT>.
|
|
863
|
+
const et = schema.elemType;
|
|
864
|
+
if (et !== undefined && plan !== undefined)
|
|
865
|
+
return `Vec<${renderTypeRef(deriveTypeRef(et, plan))}>`;
|
|
866
|
+
return "Value";
|
|
867
|
+
}
|
|
590
868
|
default:
|
|
591
869
|
return "Value";
|
|
592
870
|
}
|
|
@@ -608,14 +886,14 @@ function inputScalarKind(schema) {
|
|
|
608
886
|
}
|
|
609
887
|
/** emitInStruct — the concrete input struct declaration for a covered component (fields = inputPorts).
|
|
610
888
|
* Empty inputPorts => empty struct. Derives Default so a consumer/test can build it field-by-field. */
|
|
611
|
-
function emitInStruct(comp) {
|
|
889
|
+
function emitInStruct(comp, plan) {
|
|
612
890
|
const name = inStructName(comp.name);
|
|
613
891
|
const ports = Object.keys(comp.inputPorts ?? {});
|
|
614
892
|
if (ports.length === 0) {
|
|
615
893
|
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\n#[derive(Default)]\npub struct ${name};`;
|
|
616
894
|
}
|
|
617
895
|
const fields = ports
|
|
618
|
-
.map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p])}, // ${JSON.stringify(p)}`)
|
|
896
|
+
.map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p], plan)}, // ${JSON.stringify(p)}`)
|
|
619
897
|
.join("\n");
|
|
620
898
|
return `// ${name} — the CONCRETE input for '${comp.name}' (fields = inputPorts; typed, consumer-built —
|
|
621
899
|
// NO generic Value slice, NO per-field boxing crosses the covered read boundary).
|
|
@@ -695,8 +973,16 @@ function emitNativeScalar(node, want, resolveRef) {
|
|
|
695
973
|
* a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
|
|
696
974
|
* ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
|
|
697
975
|
*/
|
|
698
|
-
function emitPortInit(pn, expr, fieldRustType, resolveRef) {
|
|
976
|
+
function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan) {
|
|
699
977
|
const field = portFieldName(pn);
|
|
978
|
+
// #110: a componentRef port bound to a runtime array input port (with elemType) → the native Vec<ElemT>,
|
|
979
|
+
// cloned out of the concrete input struct (`in_.<field>.clone()`) — ZERO boxed Value on the read hot path.
|
|
980
|
+
if (inputPorts !== undefined && portArrayElemRef(expr, inputPorts, plan) !== null) {
|
|
981
|
+
const e = classifyExpr(expr);
|
|
982
|
+
if (e.kind === "ref" && e.path.length === 1) {
|
|
983
|
+
return `${field}: in_.${rustFieldName(e.path[0])}.clone()`;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
700
986
|
// change #2: a static string array (projection) → a native `vec![...]` of &'static str.
|
|
701
987
|
const strArr = staticStringArrElems(expr);
|
|
702
988
|
if (strArr !== null) {
|
|
@@ -765,18 +1051,21 @@ function runnerResolveRef(priorNodeAt, atPos, typedNodes, plan, inputPorts) {
|
|
|
765
1051
|
function rustScalar(s) {
|
|
766
1052
|
return s === "string" ? "String" : s === "int" ? "i64" : s === "float" ? "f64" : "bool";
|
|
767
1053
|
}
|
|
768
|
-
function emitNativeRawRunner(comp, plan,
|
|
1054
|
+
function emitNativeRawRunner(comp, plan, ap) {
|
|
769
1055
|
const ep = execPlan(comp);
|
|
770
1056
|
const order = ep.order;
|
|
771
1057
|
const hasConcurrency = ep.parallelStageOf.size > 0;
|
|
772
1058
|
const fn = sanitize(comp.name);
|
|
773
|
-
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
// a
|
|
777
|
-
//
|
|
1059
|
+
const isAsync = ap.runnerIsAsync;
|
|
1060
|
+
// bc#97: the async runner awaits each async handler at its call site (derived per-node). A real-
|
|
1061
|
+
// concurrency stage (bc#87) dispatches handlers from scoped std threads — that mechanism cannot host
|
|
1062
|
+
// `.await` (a std thread is not an async task). Fail closed LOUDLY (rather than emit code that won't
|
|
1063
|
+
// compile) iff the runner is async AND has a concurrency stage. NOTE: with per-node granularity a
|
|
1064
|
+
// consumer CAN keep a concurrency-stage component sync (declare only its OTHER terminals async) — only
|
|
1065
|
+
// a component that both concurrency-stages AND awaits is rejected. Async + concurrent-stage (issuing
|
|
1066
|
+
// the stage's futures with join!) is a documented follow-up.
|
|
778
1067
|
if (isAsync && hasConcurrency) {
|
|
779
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native async (bc#97): component '${comp.name}' has a bc#87 real-concurrency stage
|
|
1068
|
+
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.`);
|
|
780
1069
|
}
|
|
781
1070
|
const typedNodes = new Map();
|
|
782
1071
|
for (const n of comp.body)
|
|
@@ -797,39 +1086,44 @@ function emitNativeRawRunner(comp, plan, isAsync = false) {
|
|
|
797
1086
|
};
|
|
798
1087
|
const outRef = comp.outputType !== undefined ? deriveTypeRef(comp.outputType, plan) : undefined;
|
|
799
1088
|
const outTy = outRef ? renderTypeRef(outRef) : "Value";
|
|
800
|
-
// Does any
|
|
1089
|
+
// Does any expression read an input param? If not, `in_` is unused — bind `_in_` to stay clippy-clean.
|
|
1090
|
+
// #108: this must recurse ALL expression operators (cond if/then/else, map when + ports, arith/compare/…),
|
|
1091
|
+
// not just ref/concat — a cond `if:{gt:[{add:[{ref:["x"]}…]}]}` reads `in_.x` through nested operators.
|
|
801
1092
|
const readsInput = (() => {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
if (
|
|
805
|
-
return
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1093
|
+
// walk any Expression IR node; `bound` = names bound in this scope (a map $as) that are NOT input.
|
|
1094
|
+
const walk = (node, atPos, bound) => {
|
|
1095
|
+
if (node === null || typeof node === "boolean" || typeof node === "string" || typeof node === "number")
|
|
1096
|
+
return false;
|
|
1097
|
+
if (Array.isArray(node))
|
|
1098
|
+
return node.some((el) => walk(el, atPos, bound));
|
|
1099
|
+
const rec = node;
|
|
1100
|
+
const keys = Object.keys(rec);
|
|
1101
|
+
if (keys.length === 1 && (keys[0] === "ref" || keys[0] === "refOpt")) {
|
|
1102
|
+
const path = rec[keys[0]];
|
|
1103
|
+
const head = path[0];
|
|
1104
|
+
return !bound.has(head) && !priorNodeAt(head, atPos);
|
|
1105
|
+
}
|
|
1106
|
+
// any other single-key operator (obj/arr/concat/add/gt/cond/…): recurse into every child value.
|
|
1107
|
+
return keys.some((k) => walk(rec[k], atPos, bound));
|
|
812
1108
|
};
|
|
813
1109
|
return [...order.keys()].some((k) => {
|
|
814
1110
|
const nd = comp.body[order[k]];
|
|
815
1111
|
if ("map" in nd) {
|
|
816
1112
|
const m = nd.map;
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
};
|
|
829
|
-
return Object.values(m.ports).some((pn) => walkMap(pn));
|
|
1113
|
+
const bound = new Set([m.as]);
|
|
1114
|
+
// the map's OVER is evaluated in the outer scope (may read input); ports + when in element scope.
|
|
1115
|
+
if (walk(m.over, k, new Set()))
|
|
1116
|
+
return true;
|
|
1117
|
+
if (m.when !== undefined && walk(m.when, k, bound))
|
|
1118
|
+
return true;
|
|
1119
|
+
return Object.values(m.ports).some((pn) => walk(pn, k, bound));
|
|
1120
|
+
}
|
|
1121
|
+
if ("cond" in nd) {
|
|
1122
|
+
const c = nd.cond;
|
|
1123
|
+
return walk(c.if, k, new Set()) || walk(c.then, k, new Set()) || walk(c.else, k, new Set());
|
|
830
1124
|
}
|
|
831
1125
|
const cr = nd;
|
|
832
|
-
return Object.values(cr.ports).some((pn) => walk(pn, k));
|
|
1126
|
+
return Object.values(cr.ports).some((pn) => walk(pn, k, new Set()));
|
|
833
1127
|
});
|
|
834
1128
|
})();
|
|
835
1129
|
const inParam = readsInput ? "in_" : "_in_";
|
|
@@ -853,8 +1147,10 @@ function emitNativeRawRunner(comp, plan, isAsync = false) {
|
|
|
853
1147
|
// so the runner is itself `async fn` and returns a future the (already-async) consumer awaits. Sync
|
|
854
1148
|
// default emits `pub fn` exactly as before (byte-identical).
|
|
855
1149
|
const fnKw = isAsync ? "pub async fn" : "pub fn";
|
|
1150
|
+
// #108: a component whose body is ALL cond nodes dispatches NO handler — bind `_handlers` (clippy-clean).
|
|
1151
|
+
const dispatchesHandler = comp.body.some((n) => "map" in n || "component" in n);
|
|
856
1152
|
lines.push(`${fnKw} run_native_raw_struct_${fn}${bound}(`);
|
|
857
|
-
lines.push(` handlers: &H,`);
|
|
1153
|
+
lines.push(` ${dispatchesHandler ? "handlers" : "_handlers"}: &H,`);
|
|
858
1154
|
lines.push(` ${inParam}: ${inStructName(comp.name)},`);
|
|
859
1155
|
lines.push(`) -> Result<${outTy}, BehaviorError> {`);
|
|
860
1156
|
for (const k of order.keys()) {
|
|
@@ -872,7 +1168,12 @@ function emitNativeRawRunner(comp, plan, isAsync = false) {
|
|
|
872
1168
|
continue;
|
|
873
1169
|
}
|
|
874
1170
|
if ("map" in comp.body[i]) {
|
|
875
|
-
lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt,
|
|
1171
|
+
lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, ap.nodeIsAsync(comp.body[i].id)));
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
// #108: a cond node — pure Expression (no handler). Compile via the runtime-free NativeExprCompiler.
|
|
1175
|
+
if ("cond" in comp.body[i]) {
|
|
1176
|
+
lines.push(emitCondArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt));
|
|
876
1177
|
continue;
|
|
877
1178
|
}
|
|
878
1179
|
const node = comp.body[i];
|
|
@@ -914,11 +1215,14 @@ function emitNativeRawRunner(comp, plan, isAsync = false) {
|
|
|
914
1215
|
const inits = [];
|
|
915
1216
|
for (const pn of portNames) {
|
|
916
1217
|
const expr = node.ports[pn];
|
|
917
|
-
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'
|
|
918
|
-
inits.push(emitPortInit(pn, expr, ty, resolveRef));
|
|
1218
|
+
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan);
|
|
1219
|
+
inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan));
|
|
919
1220
|
}
|
|
920
1221
|
const boundArg = node.bindField !== undefined ? `bound_${s}` : "None";
|
|
921
|
-
|
|
1222
|
+
// bc#97: derived per-node await — this point-read's future is awaited HERE (its result-consumption
|
|
1223
|
+
// point) iff THIS node's terminal handler is async. A sync handler in the same async runner is called
|
|
1224
|
+
// directly (no `.await`). The runner being `async fn` does not force every call site to await.
|
|
1225
|
+
const awaitSfx = ap.nodeIsAsync(node.id) ? ".await" : "";
|
|
922
1226
|
lines.push(`${indent}let ports_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
923
1227
|
lines.push(`${indent}let row_${s} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
|
|
924
1228
|
lines.push(`${indent} Some(r) => r,`);
|
|
@@ -995,8 +1299,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
995
1299
|
const inits = [];
|
|
996
1300
|
for (const pn of Object.keys(node.ports)) {
|
|
997
1301
|
const expr = node.ports[pn];
|
|
998
|
-
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'
|
|
999
|
-
inits.push(emitPortInit(pn, expr, ty, resolveRef));
|
|
1302
|
+
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan);
|
|
1303
|
+
inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan));
|
|
1000
1304
|
}
|
|
1001
1305
|
if (gated) {
|
|
1002
1306
|
lines.push(`${ind}ports_${s} = Some(${structName} { ${inits.join(", ")} });`);
|
|
@@ -1084,26 +1388,26 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1084
1388
|
* - #93 shape 2: NON-batched map...into — dispatch ONCE PER ELEMENT (N physical requests), into per element.
|
|
1085
1389
|
* - #93 shape 3: BATCHED map NO into — dispatch once, collect each element from the concrete element row.
|
|
1086
1390
|
*/
|
|
1087
|
-
function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt,
|
|
1391
|
+
function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt,
|
|
1392
|
+
// bc#97: whether THIS map node's terminal handler is async (derived per-node) — awaits the batched /
|
|
1393
|
+
// per-element handler call at its consumption point iff true. Sync map handlers are called directly.
|
|
1394
|
+
isAsync = false) {
|
|
1088
1395
|
const m = node.map;
|
|
1089
1396
|
const awaitSfx = isAsync ? ".await" : "";
|
|
1090
1397
|
const batched = m.batched === true;
|
|
1091
1398
|
const hasInto = m.into !== undefined;
|
|
1399
|
+
const hasGuard = m.when !== undefined;
|
|
1092
1400
|
const s = sanitize(node.id);
|
|
1093
1401
|
const structName = portsStructName(comp.name, node.id);
|
|
1094
1402
|
const batch = `${structName}Batch`;
|
|
1095
|
-
|
|
1096
|
-
const
|
|
1097
|
-
const overBaseRef = typedNodes.get(overParent);
|
|
1098
|
-
const { ref: overRef } = typedFieldAccess(`${typedCell(overParent)}.borrow()`, overBaseRef, overE.kind === "ref" ? overE.path.slice(1) : [], plan);
|
|
1099
|
-
const overElemRef = overRef.elem;
|
|
1100
|
-
const overFieldPath = overE.kind === "ref" ? overE.path.slice(1).map((p) => `.${rustFieldName(p)}`).join("") : "";
|
|
1403
|
+
// #108: over element type + the OWNED over-Vec expression (prior-node arr OR input array w/ elemType).
|
|
1404
|
+
const { elemRef: overElemRef, overExpr } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
1101
1405
|
const elemMat = mapElemMaterializerName(node.id);
|
|
1102
1406
|
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1103
1407
|
const asName = m.as;
|
|
1104
1408
|
const portNames = Object.keys(m.ports);
|
|
1105
1409
|
const elemArrRef = typedNodes.get(node.id);
|
|
1106
|
-
const elemName = elemArrRef.elem
|
|
1410
|
+
const elemName = renderTypeRef(elemArrRef.elem);
|
|
1107
1411
|
const resolveRef = (head, restPath, opt) => {
|
|
1108
1412
|
if (opt)
|
|
1109
1413
|
return null;
|
|
@@ -1138,37 +1442,77 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync =
|
|
|
1138
1442
|
}
|
|
1139
1443
|
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1140
1444
|
return null;
|
|
1141
|
-
|
|
1445
|
+
// OWNED value. When restPath is EMPTY, `expr` is a bare REFERENCE (`oel_${s}` = &Elem-scalar, or
|
|
1446
|
+
// `cell.borrow()` = Ref<scalar>) — deref with `*` (Copy) / `.clone()` via deref (String). When a
|
|
1447
|
+
// `.field` was appended, `expr` is a value PLACE (auto-deref) — read it directly (`.clone()` String).
|
|
1448
|
+
const isBareRef = restPath.length === 0;
|
|
1449
|
+
let owned;
|
|
1450
|
+
if (acc.scalar === "string") {
|
|
1451
|
+
owned = isBareRef ? `(*${expr}).clone()` : `${expr}.clone()`;
|
|
1452
|
+
}
|
|
1453
|
+
else {
|
|
1454
|
+
owned = isBareRef ? `*${expr}` : expr;
|
|
1455
|
+
}
|
|
1142
1456
|
return { expr: owned, scalar: rustScalar(acc.scalar) };
|
|
1143
1457
|
};
|
|
1144
1458
|
// build the per-element CONCRETE native ports struct. `il` = indent inside the element loop.
|
|
1145
1459
|
const buildPorts = (il) => {
|
|
1146
1460
|
const out = [];
|
|
1147
1461
|
const inits = [];
|
|
1462
|
+
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1148
1463
|
for (const pn of portNames) {
|
|
1149
|
-
const ty = portFieldRustType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'
|
|
1150
|
-
inits.push(emitPortInit(pn, m.ports[pn], ty, resolveRef));
|
|
1464
|
+
const ty = portFieldRustType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding, plan);
|
|
1465
|
+
inits.push(emitPortInit(pn, m.ports[pn], ty, resolveRef, comp.inputPorts, plan));
|
|
1151
1466
|
}
|
|
1152
1467
|
out.push(`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
1153
1468
|
return out;
|
|
1154
1469
|
};
|
|
1470
|
+
// #108 guard: compile `when` to a native bool over an element-scoped resolveHead ($as element value,
|
|
1471
|
+
// prior node, input port). On skip: `continue` (element excluded — matches run_behavior's keep-gate).
|
|
1472
|
+
const emitGuard = (il) => {
|
|
1473
|
+
if (!hasGuard)
|
|
1474
|
+
return [];
|
|
1475
|
+
const resolveHead = (head) => {
|
|
1476
|
+
if (head === asName)
|
|
1477
|
+
return { expr: `oel_${s}.clone()`, ref: overElemRef };
|
|
1478
|
+
if (priorHere(head))
|
|
1479
|
+
return { expr: `${typedCell(head)}.borrow().clone()`, ref: typedNodes.get(head) };
|
|
1480
|
+
return rustInputHeadRef(head, comp, plan);
|
|
1481
|
+
};
|
|
1482
|
+
const g = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compileBool(m.when);
|
|
1483
|
+
const out = [];
|
|
1484
|
+
for (const st of g.stmts)
|
|
1485
|
+
out.push(`${il}${st}`);
|
|
1486
|
+
// bind the keep flag to a var so clippy's nonminimal_bool doesn't rewrite `!(a > b)` (the `when`
|
|
1487
|
+
// expr must stay verbatim = run_behavior's `{cond:[when,true,false]}` strict-bool keep-gate).
|
|
1488
|
+
out.push(`${il}let __keep = ${rustStripOuterParens(g.expr)};`);
|
|
1489
|
+
out.push(`${il}if !__keep { continue; }`);
|
|
1490
|
+
return out;
|
|
1491
|
+
};
|
|
1155
1492
|
const lines = [];
|
|
1156
1493
|
const closers = [];
|
|
1157
|
-
lines.push(` // ── map '${node.id}' (${m.component}, ${batched ? "batched" : "per-element"}, into:${JSON.stringify(m.into ?? null)}${m.relationKind ? ", relationKind:" + m.relationKind : ""}${m.parent ? ", parent:" + m.parent : ""}) ──`);
|
|
1494
|
+
lines.push(` // ── map '${node.id}' (${m.component}, ${batched ? "batched" : "per-element"}, into:${JSON.stringify(m.into ?? null)}${hasGuard ? ", guarded" : ""}${m.relationKind ? ", relationKind:" + m.relationKind : ""}${m.parent ? ", parent:" + m.parent : ""}) ──`);
|
|
1158
1495
|
let indent = " ";
|
|
1159
1496
|
if (m.parent !== undefined && priorNodeAt(m.parent, atPos)) {
|
|
1160
1497
|
lines.push(`${indent}if produced_${sanitize(m.parent)}.get() {`);
|
|
1161
1498
|
closers.unshift(`${indent}}`);
|
|
1162
1499
|
indent += " ";
|
|
1163
1500
|
}
|
|
1164
|
-
lines.push(`${indent}let over_${s} = ${
|
|
1501
|
+
lines.push(`${indent}let over_${s} = ${overExpr};`);
|
|
1165
1502
|
lines.push(`${indent}let mut built_${s}: Vec<${elemName}> = Vec::with_capacity(over_${s}.len());`);
|
|
1166
1503
|
if (batched) {
|
|
1504
|
+
// build kept-element ports (+ kept over elements for into). Batched into is never guarded (rejected).
|
|
1167
1505
|
lines.push(`${indent}let mut items_${s}: Vec<${structName}> = Vec::with_capacity(over_${s}.len());`);
|
|
1506
|
+
if (hasInto)
|
|
1507
|
+
lines.push(`${indent}let mut kept_${s}: Vec<${renderTypeRef(overElemRef)}> = Vec::with_capacity(over_${s}.len());`);
|
|
1168
1508
|
lines.push(`${indent}for oel_${s} in over_${s}.iter() {`);
|
|
1509
|
+
for (const l of emitGuard(indent + " "))
|
|
1510
|
+
lines.push(l);
|
|
1169
1511
|
for (const l of buildPorts(indent + " "))
|
|
1170
1512
|
lines.push(l);
|
|
1171
1513
|
lines.push(`${indent} items_${s}.push(ep_${s});`);
|
|
1514
|
+
if (hasInto)
|
|
1515
|
+
lines.push(`${indent} kept_${s}.push(oel_${s}.clone());`);
|
|
1172
1516
|
lines.push(`${indent}}`);
|
|
1173
1517
|
lines.push(`${indent}if !items_${s}.is_empty() {`);
|
|
1174
1518
|
lines.push(`${indent} let want_${s} = items_${s}.len();`);
|
|
@@ -1183,23 +1527,28 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync =
|
|
|
1183
1527
|
lines.push(`${indent} if row_${s}.rows.len() != want_${s} {`);
|
|
1184
1528
|
lines.push(`${indent} return Err(BehaviorError::new("MAP_BATCH_RESULT_MISMATCH", format!("map '{}': batched handler must return a list aligned to items (want {})", ${rustStrLit(node.id)}, want_${s})));`);
|
|
1185
1529
|
lines.push(`${indent} }`);
|
|
1186
|
-
lines.push(`${indent} let mut rows_iter_${s} = row_${s}.rows.into_iter();`);
|
|
1187
|
-
lines.push(`${indent} for over_el_${s} in over_${s}.iter() {`);
|
|
1188
|
-
lines.push(`${indent} let er_${s} = rows_iter_${s}.next().expect("aligned batch row");`);
|
|
1189
1530
|
if (hasInto) {
|
|
1531
|
+
// zip the aligned batch rows onto each KEPT over element (into materializer).
|
|
1532
|
+
lines.push(`${indent} let mut rows_iter_${s} = row_${s}.rows.into_iter();`);
|
|
1533
|
+
lines.push(`${indent} for over_el_${s} in kept_${s}.iter() {`);
|
|
1534
|
+
lines.push(`${indent} let er_${s} = rows_iter_${s}.next().expect("aligned batch row");`);
|
|
1190
1535
|
lines.push(`${indent} built_${s}.push(${elemMat}(over_el_${s}, er_${s}));`);
|
|
1536
|
+
lines.push(`${indent} }`);
|
|
1191
1537
|
}
|
|
1192
1538
|
else {
|
|
1193
|
-
|
|
1539
|
+
// no-into: collect each element DIRECTLY from the aligned per-element row (result = kept rows).
|
|
1540
|
+
lines.push(`${indent} for er_${s} in row_${s}.rows.into_iter() {`);
|
|
1194
1541
|
lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
|
|
1195
1542
|
lines.push(`${indent} built_${s}.push(el_${s});`);
|
|
1543
|
+
lines.push(`${indent} }`);
|
|
1196
1544
|
}
|
|
1197
|
-
lines.push(`${indent} }`);
|
|
1198
1545
|
lines.push(`${indent}}`);
|
|
1199
1546
|
}
|
|
1200
1547
|
else {
|
|
1201
|
-
// #93 shape 2: NON-batched map
|
|
1548
|
+
// #93 shape 2 / #108: NON-batched map — dispatch ONCE PER KEPT ELEMENT.
|
|
1202
1549
|
lines.push(`${indent}for oel_${s} in over_${s}.iter() {`);
|
|
1550
|
+
for (const l of emitGuard(indent + " "))
|
|
1551
|
+
lines.push(l);
|
|
1203
1552
|
for (const l of buildPorts(indent + " "))
|
|
1204
1553
|
lines.push(l);
|
|
1205
1554
|
lines.push(`${indent} let er_${s} = match handlers.${nodeMethodName(node.id)}(&ep_${s}, None)${awaitSfx} {`);
|
|
@@ -1213,6 +1562,7 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync =
|
|
|
1213
1562
|
lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s}, er_${s}));`);
|
|
1214
1563
|
}
|
|
1215
1564
|
else {
|
|
1565
|
+
lines.push(`${indent} let _ = &oel_${s};`);
|
|
1216
1566
|
lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
|
|
1217
1567
|
lines.push(`${indent} built_${s}.push(el_${s});`);
|
|
1218
1568
|
}
|
|
@@ -1224,6 +1574,71 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync =
|
|
|
1224
1574
|
lines.push(c);
|
|
1225
1575
|
return lines.join("\n");
|
|
1226
1576
|
}
|
|
1577
|
+
/** #108: resolve an INPUT port head for a native-expr ref (rust). Scalar → `in_.<field>` (owned: String
|
|
1578
|
+
* cloned, Copy as-is); array WITH elemType → `in_.<field>.clone()` of Vec<ElemT> (for `len`); else null. */
|
|
1579
|
+
function rustInputHeadRef(head, comp, plan) {
|
|
1580
|
+
const schema = comp.inputPorts?.[head];
|
|
1581
|
+
if (schema === undefined)
|
|
1582
|
+
return null;
|
|
1583
|
+
const sc = inputScalarKind(schema);
|
|
1584
|
+
if (sc !== undefined) {
|
|
1585
|
+
const scalarRef = { kind: "scalar", scalar: sc === "i64" ? "int" : sc === "f64" ? "float" : sc === "String" ? "string" : "bool" };
|
|
1586
|
+
const field = `in_.${rustFieldName(head)}`;
|
|
1587
|
+
return { expr: sc === "String" ? `${field}.clone()` : field, ref: scalarRef };
|
|
1588
|
+
}
|
|
1589
|
+
const et = schema.elemType;
|
|
1590
|
+
if ((schema.type === "array" || schema.type === "arr") && et !== undefined) {
|
|
1591
|
+
// no clone: the only array use inside a cond/guard expr is `len`, which BORROWS (`&in_.xs`).
|
|
1592
|
+
return { expr: `in_.${rustFieldName(head)}`, ref: { kind: "arr", elem: deriveTypeRef(et, plan) } };
|
|
1593
|
+
}
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* emitCondArm — the struct-native exec of a covered cond node (#108, rust). Mirrors run_behavior's cond
|
|
1598
|
+
* lower: compile `if` to a native bool, materialize the TAKEN branch to the node's outType (only the
|
|
1599
|
+
* taken branch evaluated), assign the node's typed cell + set produced. NO handler dispatch. Compiles
|
|
1600
|
+
* via the runtime-free NativeExprCompiler (byte-equal to `evaluate`); an uncovered expr fails closed.
|
|
1601
|
+
*/
|
|
1602
|
+
function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
|
|
1603
|
+
const c = node.cond;
|
|
1604
|
+
const s = sanitize(node.id);
|
|
1605
|
+
const nodeRef = typedNodes.get(node.id);
|
|
1606
|
+
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1607
|
+
const resolveHead = (head) => {
|
|
1608
|
+
if (priorHere(head))
|
|
1609
|
+
return { expr: `${typedCell(head)}.borrow().clone()`, ref: typedNodes.get(head) };
|
|
1610
|
+
return rustInputHeadRef(head, comp, plan);
|
|
1611
|
+
};
|
|
1612
|
+
const compiler = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan));
|
|
1613
|
+
const resolveDecl = (name) => findDecl(plan, name).fields;
|
|
1614
|
+
const cond = compiler.compileBool(c.if);
|
|
1615
|
+
const thenC = compiler.compileTo(c.then, nodeRef, resolveDecl);
|
|
1616
|
+
const elseC = compiler.compileTo(c.else, nodeRef, resolveDecl);
|
|
1617
|
+
const lines = [];
|
|
1618
|
+
lines.push(` // ── cond '${node.id}'${c.parent ? " (parent:" + c.parent + ")" : ""} — pure Expression (no handler) ──`);
|
|
1619
|
+
let indent = " ";
|
|
1620
|
+
const closers = [];
|
|
1621
|
+
if (c.parent !== undefined && priorNodeAt(c.parent, atPos)) {
|
|
1622
|
+
lines.push(`${indent}if produced_${sanitize(c.parent)}.get() {`);
|
|
1623
|
+
closers.unshift(`${indent}}`);
|
|
1624
|
+
indent += " ";
|
|
1625
|
+
}
|
|
1626
|
+
for (const st of cond.stmts)
|
|
1627
|
+
lines.push(`${indent}${st}`);
|
|
1628
|
+
lines.push(`${indent}if ${rustStripOuterParens(cond.expr)} {`);
|
|
1629
|
+
for (const st of thenC.stmts)
|
|
1630
|
+
lines.push(`${indent} ${st}`);
|
|
1631
|
+
lines.push(`${indent} *${typedCell(node.id)}.borrow_mut() = ${thenC.expr};`);
|
|
1632
|
+
lines.push(`${indent}} else {`);
|
|
1633
|
+
for (const st of elseC.stmts)
|
|
1634
|
+
lines.push(`${indent} ${st}`);
|
|
1635
|
+
lines.push(`${indent} *${typedCell(node.id)}.borrow_mut() = ${elseC.expr};`);
|
|
1636
|
+
lines.push(`${indent}}`);
|
|
1637
|
+
lines.push(`${indent}produced_${s}.set(true);`);
|
|
1638
|
+
for (const cl of closers)
|
|
1639
|
+
lines.push(cl);
|
|
1640
|
+
return lines.join("\n");
|
|
1641
|
+
}
|
|
1227
1642
|
/** emitElemMove — move a concrete element row (RawElemNR, error already handled) into a fresh `let` local
|
|
1228
1643
|
* of the element outType. Named → struct literal from moved fields; scalar/arr/opt → .val. */
|
|
1229
1644
|
function emitElemMove(letBind, rowExpr, ref, plan) {
|
|
@@ -1306,8 +1721,11 @@ fn unknown_component(component: &str) -> BehaviorError {
|
|
|
1306
1721
|
}`;
|
|
1307
1722
|
// ── module assembly ──────────────────────────────────────────────────────────────────
|
|
1308
1723
|
function emit(ctx) {
|
|
1309
|
-
// bc#97
|
|
1310
|
-
|
|
1724
|
+
// bc#97 (increment 2): the SSoT language-agnostic propagation pass derives, PER COMPONENT + PER NODE,
|
|
1725
|
+
// which handler-call boundaries must be awaited (bottom-up over the declared async terminal handlers).
|
|
1726
|
+
// rust CONSUMES this annotation (async fn + .await where derived); go ignores it. per-node granularity
|
|
1727
|
+
// is a strict generalization of increment-1's uniform ioModel:'async' (= every terminal async).
|
|
1728
|
+
const asyncPlans = buildAsyncPlans(ctx.ir, ctx.ioModel);
|
|
1311
1729
|
for (const c of ctx.ir.components)
|
|
1312
1730
|
assertTypedCoverage(c);
|
|
1313
1731
|
const native = ctx.ir.components.filter(isNativeRaw);
|
|
@@ -1342,6 +1760,8 @@ ${emitStructSurface([])}
|
|
|
1342
1760
|
.replace(/^( {4})([a-z_][A-Za-z0-9_]*: )/gm, "$1pub $2")
|
|
1343
1761
|
.replace(/^( {4})(r#[a-z_][A-Za-z0-9_]*: )/gm, "$1pub $2");
|
|
1344
1762
|
const defaults = emitDefaults(plan);
|
|
1763
|
+
// #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler).
|
|
1764
|
+
rustExprUsed.helpers = false;
|
|
1345
1765
|
const structs = [];
|
|
1346
1766
|
const rowStructs = [];
|
|
1347
1767
|
const handlerTraits = [];
|
|
@@ -1349,21 +1769,26 @@ ${emitStructSurface([])}
|
|
|
1349
1769
|
const elemMaterializers = [];
|
|
1350
1770
|
for (const c of native) {
|
|
1351
1771
|
const typedNodes = new Map();
|
|
1772
|
+
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
1773
|
+
for (const n of c.body)
|
|
1774
|
+
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1352
1775
|
for (const n of c.body) {
|
|
1353
1776
|
if ("map" in n)
|
|
1354
|
-
structs.push(emitMapPortsStructs(c, n));
|
|
1777
|
+
structs.push(emitMapPortsStructs(c, n, typedNodes, plan));
|
|
1778
|
+
else if ("cond" in n) {
|
|
1779
|
+
// #108: a cond node has NO ports struct (pure Expression — no handler dispatch).
|
|
1780
|
+
}
|
|
1355
1781
|
else
|
|
1356
|
-
structs.push(emitPortsStruct(c, n));
|
|
1357
|
-
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1782
|
+
structs.push(emitPortsStruct(c, n, undefined, plan));
|
|
1358
1783
|
}
|
|
1359
1784
|
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
1360
|
-
handlerTraits.push(emitHandlerTrait(c, typedNodes, plan,
|
|
1361
|
-
inStructs.push(emitInStruct(c));
|
|
1785
|
+
handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
|
|
1786
|
+
inStructs.push(emitInStruct(c, plan));
|
|
1362
1787
|
const mm = emitMapElemMaterializers(c, typedNodes, plan);
|
|
1363
1788
|
if (mm)
|
|
1364
1789
|
elemMaterializers.push(mm);
|
|
1365
1790
|
}
|
|
1366
|
-
const runners = native.map((c) => emitNativeRawRunner(c, plan,
|
|
1791
|
+
const runners = native.map((c) => emitNativeRawRunner(c, plan, asyncPlans.get(c.name))).join("\n\n");
|
|
1367
1792
|
const structSurface = emitStructSurface(native);
|
|
1368
1793
|
// (nonNative.length === 0 is guaranteed here — the fail-closed guard above threw otherwise.)
|
|
1369
1794
|
const banner = `#![allow(dead_code, unused_imports, non_snake_case)]
|
|
@@ -1389,8 +1814,15 @@ use std::cell::RefCell;`;
|
|
|
1389
1814
|
decls || undefined,
|
|
1390
1815
|
defaults || undefined,
|
|
1391
1816
|
UNKNOWN_COMPONENT_HELPER,
|
|
1392
|
-
|
|
1393
|
-
|
|
1817
|
+
rustExprUsed.helpers
|
|
1818
|
+
? `// #108 native-expr helper library (runtime-free; byte-equal to expr-eval.ts core) — used by\n// cond / map-guard checked arithmetic. NO bc-runtime reference (failures use the LOCAL BehaviorError).\n${emitExprHelpersRust()}`
|
|
1819
|
+
: undefined,
|
|
1820
|
+
structs.some((r) => r.trim().length > 0)
|
|
1821
|
+
? `// Native ports structs (one per componentRef node; typed per the static port type — CONCRETE).\n${structs.filter((r) => r.trim().length > 0).join("\n\n")}`
|
|
1822
|
+
: undefined,
|
|
1823
|
+
rowStructs.some((r) => r.trim().length > 0)
|
|
1824
|
+
? `// CONCRETE per-node row structs (typed fields = the node's outType; + error signal).\n${rowStructs.filter((r) => r.trim().length > 0).join("\n\n")}`
|
|
1825
|
+
: undefined,
|
|
1394
1826
|
`// CONCRETE per-component input structs (fields = inputPorts).\n${inStructs.join("\n\n")}`,
|
|
1395
1827
|
`// CONCRETE per-component handler traits (one node_* method per node — native ports IN, row OUT).\n${handlerTraits.join("\n\n")}`,
|
|
1396
1828
|
elemMaterializers.length
|
|
@@ -1410,13 +1842,18 @@ use std::cell::RefCell;`;
|
|
|
1410
1842
|
* Value for the equivalence pin. Lives OUTSIDE the covered module so the module stays literal whole-file
|
|
1411
1843
|
* zero (the ser_* / decode helpers are test glue, never on the consumer's read hot path).
|
|
1412
1844
|
*/
|
|
1413
|
-
export function rustTypedNativeObserve(ir) {
|
|
1845
|
+
export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
1414
1846
|
const components = ir.components;
|
|
1415
1847
|
const native = components.filter(isNativeRaw);
|
|
1416
1848
|
const plan = buildTypePlan(ir);
|
|
1417
1849
|
const needSync = native.some((c) => concurrencyPlan(c) !== null);
|
|
1418
1850
|
const serializers = emitSerializers(plan);
|
|
1419
1851
|
const marshallers = emitMarshallers(plan);
|
|
1852
|
+
// bc#97: the observe glue must MATCH the covered module's per-node async-ness — the scripted handler's
|
|
1853
|
+
// node_* methods are `async fn` exactly where the trait's are, and observe_native_raw awaits an async
|
|
1854
|
+
// runner. Built from the SAME SSoT propagation pass the covered emitter used, so glue never drifts.
|
|
1855
|
+
const asyncPlans = buildAsyncPlans(ir, ioModel);
|
|
1856
|
+
const anyAsyncRunner = native.some((c) => asyncPlans.get(c.name).runnerIsAsync);
|
|
1420
1857
|
// ── per-component input decoders: generic &[(String,Value)] -> concrete InNR_<comp> (test glue). ──
|
|
1421
1858
|
const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
|
|
1422
1859
|
// ── concrete scripted handler: implements every HandlerNR<comp> trait by decoding a scripted Value
|
|
@@ -1425,12 +1862,18 @@ export function rustTypedNativeObserve(ir) {
|
|
|
1425
1862
|
const emittedDecode = new Set();
|
|
1426
1863
|
const traitImpls = [];
|
|
1427
1864
|
for (const c of native) {
|
|
1865
|
+
const ap = asyncPlans.get(c.name);
|
|
1866
|
+
// the scripted impl's node_* is `async fn` iff the trait's is (same per-node derivation).
|
|
1867
|
+
const fnKwFor = (nodeId) => (ap.nodeIsAsync(nodeId) ? "async fn" : "fn");
|
|
1428
1868
|
const typedNodes = new Map();
|
|
1429
1869
|
for (const n of c.body)
|
|
1430
1870
|
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1431
1871
|
const methods = [];
|
|
1432
1872
|
for (const n of c.body) {
|
|
1873
|
+
if ("cond" in n)
|
|
1874
|
+
continue; // #108: a cond node has no handler method (pure Expression) — no scripted impl.
|
|
1433
1875
|
const ref = typedNodes.get(n.id);
|
|
1876
|
+
const fnKw = fnKwFor(n.id);
|
|
1434
1877
|
// bc#90 / runtime-free: the scripted impl's `_bound` param mirrors the trait's typed produced-aware
|
|
1435
1878
|
// Option<K> (NOT a boxed Value) — computed per node so the impl signature matches the trait.
|
|
1436
1879
|
const bt = boundRustType(n, typedNodes, plan);
|
|
@@ -1445,7 +1888,7 @@ export function rustTypedNativeObserve(ir) {
|
|
|
1445
1888
|
const portsT = batched ? `${portsStructName(c.name, n.id)}Batch` : portsStructName(c.name, n.id);
|
|
1446
1889
|
const retT = batched ? batchT : elemT;
|
|
1447
1890
|
if (batched) {
|
|
1448
|
-
methods.push(`
|
|
1891
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
|
|
1449
1892
|
let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
|
|
1450
1893
|
if is_err {
|
|
1451
1894
|
return Some(${batchT} { is_error: true, err, ..Default::default() });
|
|
@@ -1456,7 +1899,7 @@ export function rustTypedNativeObserve(ir) {
|
|
|
1456
1899
|
}`);
|
|
1457
1900
|
}
|
|
1458
1901
|
else {
|
|
1459
|
-
methods.push(`
|
|
1902
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${elemT}> {
|
|
1460
1903
|
let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
|
|
1461
1904
|
if is_err {
|
|
1462
1905
|
return Some(${elemT} { is_error: true, err, ..Default::default() });
|
|
@@ -1471,7 +1914,7 @@ export function rustTypedNativeObserve(ir) {
|
|
|
1471
1914
|
const node = n;
|
|
1472
1915
|
const rowT = rawRowStructName(c.name, node.id);
|
|
1473
1916
|
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
1474
|
-
methods.push(`
|
|
1917
|
+
methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, _ports: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
|
|
1475
1918
|
let (is_err, err, ok) = self.next(${rustStrLit(node.component)})?;
|
|
1476
1919
|
if is_err {
|
|
1477
1920
|
return Some(${rowT} { is_error: true, err, ..Default::default() });
|
|
@@ -1490,7 +1933,12 @@ export function rustTypedNativeObserve(ir) {
|
|
|
1490
1933
|
// bridge it to the bc-runtime `behavior_contracts::BehaviorError` at the observe boundary (same
|
|
1491
1934
|
// code/message) so the conformance harness (main.rs) reads the stable code without the covered
|
|
1492
1935
|
// module ever touching bc runtime. decode_* is test glue and returns the local error too.
|
|
1493
|
-
|
|
1936
|
+
// bc#97: an async runner returns a Future — drive it to completion with the std-only block-on
|
|
1937
|
+
// (bc_block_on) so observe_native_raw keeps its SYNC external signature (the harness is unchanged).
|
|
1938
|
+
// The scripted async node_* return ready values, so the future needs no real executor / no runtime.
|
|
1939
|
+
const runnerCall = `super::run_native_raw_struct_${sanitize(c.name)}(handlers, in_)`;
|
|
1940
|
+
const driven = asyncPlans.get(c.name).runnerIsAsync ? `bc_block_on(${runnerCall})` : runnerCall;
|
|
1941
|
+
return ` ${rustStrLit(c.name)} => {\n let in_ = decode_${inStructName(c.name)}(input).map_err(to_rt_err)?;\n let tv = ${driven}.map_err(to_rt_err)?;\n Ok(${ser})\n }`;
|
|
1494
1942
|
})
|
|
1495
1943
|
.join("\n");
|
|
1496
1944
|
// the scripted handler is passed as `&ScriptedNativeRaw`; the observe entry takes `&ScriptedNativeRaw`
|
|
@@ -1562,6 +2010,30 @@ use super::*;
|
|
|
1562
2010
|
fn to_rt_err(e: BehaviorError) -> behavior_contracts::BehaviorError {
|
|
1563
2011
|
behavior_contracts::BehaviorError::new(e.code, e.message)
|
|
1564
2012
|
}
|
|
2013
|
+
${anyAsyncRunner
|
|
2014
|
+
? `// bc#97 (TEST glue) — a minimal std-only block-on that drives an async runner Future to completion so
|
|
2015
|
+
// observe_native_raw stays sync-callable (the harness never changes). The covered async fn's futures are
|
|
2016
|
+
// ready (the scripted handler does no real I/O), so a no-op waker + spin poll resolves in one step. This
|
|
2017
|
+
// lives ONLY in the observe companion — the covered module itself imports zero runtime.
|
|
2018
|
+
fn bc_block_on<F: std::future::Future>(mut fut: F) -> F::Output {
|
|
2019
|
+
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
|
2020
|
+
fn noop(_: *const ()) {}
|
|
2021
|
+
fn clone(_: *const ()) -> RawWaker {
|
|
2022
|
+
RawWaker::new(std::ptr::null(), &VTABLE)
|
|
2023
|
+
}
|
|
2024
|
+
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);
|
|
2025
|
+
let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
|
|
2026
|
+
let mut cx = Context::from_waker(&waker);
|
|
2027
|
+
// SAFETY: fut is owned on the stack and never moved after pinning (it is dropped at scope end).
|
|
2028
|
+
let mut fut = unsafe { std::pin::Pin::new_unchecked(&mut fut) };
|
|
2029
|
+
loop {
|
|
2030
|
+
match fut.as_mut().poll(&mut cx) {
|
|
2031
|
+
Poll::Ready(v) => return v,
|
|
2032
|
+
Poll::Pending => std::thread::yield_now(),
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
}`
|
|
2036
|
+
: ""}
|
|
1565
2037
|
|
|
1566
2038
|
${serializers}
|
|
1567
2039
|
|
|
@@ -1601,7 +2073,6 @@ ${arms}
|
|
|
1601
2073
|
* typed read; Value-typed → the raw Value clone). A missing key leaves Default (every covered input is
|
|
1602
2074
|
* required + supplied by the corpus). Lives ONLY in the observe companion. */
|
|
1603
2075
|
function emitInDecoder(comp, plan) {
|
|
1604
|
-
void plan;
|
|
1605
2076
|
const name = inStructName(comp.name);
|
|
1606
2077
|
const ports = Object.keys(comp.inputPorts ?? {});
|
|
1607
2078
|
const lines = [];
|
|
@@ -1617,10 +2088,26 @@ function emitInDecoder(comp, plan) {
|
|
|
1617
2088
|
lines.push(` for (k, v) in input {`);
|
|
1618
2089
|
ports.forEach((p, i) => {
|
|
1619
2090
|
const field = rustFieldName(p);
|
|
1620
|
-
const
|
|
2091
|
+
const schema = comp.inputPorts[p];
|
|
2092
|
+
const scalar = inputScalarKind(schema);
|
|
2093
|
+
const et = schema.elemType;
|
|
1621
2094
|
const head = i === 0 ? " if" : " } else if";
|
|
1622
2095
|
lines.push(`${head} k == ${rustStrLit(p)} {`);
|
|
1623
|
-
if (
|
|
2096
|
+
if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
2097
|
+
// #108: decode the boxed Value::Arr into the native Vec<ElemT> (test glue — materialize each elem).
|
|
2098
|
+
const elemRef = deriveTypeRef(et, plan);
|
|
2099
|
+
const elemTy = renderTypeRef(elemRef);
|
|
2100
|
+
const mat = materializeExpr("ev", elemRef, plan);
|
|
2101
|
+
lines.push(` match v {`);
|
|
2102
|
+
lines.push(` Value::Arr(a) => {`);
|
|
2103
|
+
lines.push(` let mut dec: Vec<${elemTy}> = Vec::with_capacity(a.len());`);
|
|
2104
|
+
lines.push(` for ev in a.iter() { dec.push(${mat}); }`);
|
|
2105
|
+
lines.push(` in_.${field} = dec;`);
|
|
2106
|
+
lines.push(` }`);
|
|
2107
|
+
lines.push(` other => return Err(marshal_type_error(${rustStrLit(`decode input ${p}: expected array, got `)}, other.type_name())),`);
|
|
2108
|
+
lines.push(` }`);
|
|
2109
|
+
}
|
|
2110
|
+
else if (scalar === undefined) {
|
|
1624
2111
|
lines.push(` in_.${field} = v.clone();`);
|
|
1625
2112
|
}
|
|
1626
2113
|
else {
|