behavior-contracts 0.5.0 → 0.6.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 +495 -75
- 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 +553 -103
- 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,7 @@ function staticArrElems(node) {
|
|
|
84
86
|
const arg = node.arr;
|
|
85
87
|
return Array.isArray(arg) ? arg : null;
|
|
86
88
|
}
|
|
87
|
-
function inferPortType(node, inputPorts) {
|
|
89
|
+
function inferPortType(node, inputPorts, asBinding) {
|
|
88
90
|
if (staticArrElems(node) !== null)
|
|
89
91
|
return "Value";
|
|
90
92
|
const e = classifyExpr(node);
|
|
@@ -95,6 +97,18 @@ function inferPortType(node, inputPorts) {
|
|
|
95
97
|
case "bool":
|
|
96
98
|
return "bool";
|
|
97
99
|
case "ref": {
|
|
100
|
+
// #108: a `$as`-headed ref (map element binding) — resolve the element field's scalar type.
|
|
101
|
+
if (asBinding !== undefined && e.path[0] === asBinding.name) {
|
|
102
|
+
try {
|
|
103
|
+
const { ref } = typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan);
|
|
104
|
+
if (ref.kind === "scalar" && ref.scalar !== "null")
|
|
105
|
+
return rustScalar(ref.scalar);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return "Value";
|
|
109
|
+
}
|
|
110
|
+
return "Value";
|
|
111
|
+
}
|
|
98
112
|
if (e.path.length === 1) {
|
|
99
113
|
const s = inputPorts[e.path[0]];
|
|
100
114
|
if (s) {
|
|
@@ -139,13 +153,13 @@ function staticStringArrElems(node) {
|
|
|
139
153
|
* Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no `Value`
|
|
140
154
|
* fallback on the native module). `where` names the emission site for the error.
|
|
141
155
|
*/
|
|
142
|
-
function portFieldRustType(node, inputPorts, where = "port") {
|
|
156
|
+
function portFieldRustType(node, inputPorts, where = "port", asBinding) {
|
|
143
157
|
if (staticStringArrElems(node) !== null)
|
|
144
158
|
return "Vec<&'static str>";
|
|
145
159
|
const num = numLiteralRustExpr(node);
|
|
146
160
|
if (num !== null)
|
|
147
161
|
return typeof node === "number" && Number.isInteger(node) ? "i64" : "f64";
|
|
148
|
-
const ft = inferPortType(node, inputPorts);
|
|
162
|
+
const ft = inferPortType(node, inputPorts, asBinding);
|
|
149
163
|
if (ft === "Value") {
|
|
150
164
|
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
165
|
}
|
|
@@ -158,13 +172,13 @@ function portFieldRustType(node, inputPorts, where = "port") {
|
|
|
158
172
|
* reads `ports.f_<field>` DIRECTLY. The struct derives Clone (a bc#87 parallel stage clones it into the
|
|
159
173
|
* per-worker dispatch). Every field is a NATIVE Rust type (bc#90 / runtime-free) — NO boxed `Value`.
|
|
160
174
|
*/
|
|
161
|
-
function emitPortsStruct(comp, node) {
|
|
175
|
+
function emitPortsStruct(comp, node, asBinding) {
|
|
162
176
|
const name = portsStructName(comp.name, node.id);
|
|
163
177
|
const portNames = Object.keys(node.ports);
|
|
164
178
|
if (portNames.length === 0) {
|
|
165
179
|
return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
|
|
166
180
|
}
|
|
167
|
-
const types = portNames.map((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'
|
|
181
|
+
const types = portNames.map((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding));
|
|
168
182
|
const fields = portNames
|
|
169
183
|
.map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
|
|
170
184
|
.join("\n");
|
|
@@ -181,11 +195,14 @@ ${fields}
|
|
|
181
195
|
* struct carries the Vec of per-element CONCRETE ports structs (the handler reads them off the concrete
|
|
182
196
|
* struct — no by-name accessor, no key-value object).
|
|
183
197
|
*/
|
|
184
|
-
function emitMapPortsStructs(comp, node) {
|
|
198
|
+
function emitMapPortsStructs(comp, node, typedNodes, plan) {
|
|
185
199
|
const m = node.map;
|
|
186
200
|
const name = portsStructName(comp.name, node.id);
|
|
187
201
|
const batch = `${name}Batch`;
|
|
188
|
-
|
|
202
|
+
// #108: element ports may read the `$as` binding — resolve the over element type for native typing.
|
|
203
|
+
const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
204
|
+
const asBinding = { name: m.as, ref: elemRef, plan };
|
|
205
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding);
|
|
189
206
|
return `${elemStruct}
|
|
190
207
|
|
|
191
208
|
// ${batch} — CONCRETE batched ports for map '${node.id}': the Vec of per-element CONCRETE ports structs
|
|
@@ -196,6 +213,188 @@ pub struct ${batch} {
|
|
|
196
213
|
pub items: Vec<${name}>,
|
|
197
214
|
}`;
|
|
198
215
|
}
|
|
216
|
+
// ── native expression compiler backend (#108) — cond `if` / map `when` / cond branches (Rust) ────────
|
|
217
|
+
//
|
|
218
|
+
// The runtime-free native expression compiler (native-expr.ts) mirrors run_behavior's cond/guard with
|
|
219
|
+
// BYTE-EQUAL semantics. The value-dependent runtime failures (INT_OVERFLOW / NAN_OR_INF / MOD_ZERO /
|
|
220
|
+
// PRECISION_LOSS) are emitted as calls to a FIXED set of RUNTIME-FREE helper fns baked into the module
|
|
221
|
+
// (bc_expr_* below) returning Result<T, BehaviorError> — the covered module still imports ZERO bc runtime.
|
|
222
|
+
// The runner is Result<_, BehaviorError>, so a fallible helper propagates with `?`.
|
|
223
|
+
let rustExprTempSeq = 0;
|
|
224
|
+
/** module-level accumulator: set when the native-expr compiler emits a fallible helper call (so emit()
|
|
225
|
+
* bakes the helper library). Reset at the start of each emit(). */
|
|
226
|
+
const rustExprUsed = { helpers: false };
|
|
227
|
+
/** makeRustExprBackend — renders native-expr for Rust. `resolveHead` maps a ref head to an OWNED typed
|
|
228
|
+
* base expr (a prior-node cell clone / an input field / a $as element). Sets rustExprUsed.helpers when
|
|
229
|
+
* a fallible helper is emitted. The runner is Result<_, BehaviorError>, so hoistFallible uses `?`. */
|
|
230
|
+
function makeRustExprBackend(resolveHead, plan) {
|
|
231
|
+
const used = rustExprUsed;
|
|
232
|
+
return {
|
|
233
|
+
lang: "rust",
|
|
234
|
+
freshTemp: () => `ex_${rustExprTempSeq++}`,
|
|
235
|
+
renderType: (ref) => renderTypeRef(ref),
|
|
236
|
+
scalarType: (s) => rustScalar(s),
|
|
237
|
+
strLit: (s) => `${rustStrLit(s)}.to_string()`,
|
|
238
|
+
boolLit: (b) => (b ? "true" : "false"),
|
|
239
|
+
intLit: (dec) => `${dec}i64`,
|
|
240
|
+
floatLit: (n) => `${rustFloatLitExpr(n)}f64`,
|
|
241
|
+
fieldAccess: (baseExpr, baseRef, path) => typedFieldAccess(baseExpr, baseRef, path, plan),
|
|
242
|
+
resolveHead,
|
|
243
|
+
hoistFallible: (temp, _ty, call) => {
|
|
244
|
+
used.helpers = true;
|
|
245
|
+
return [`let ${temp} = ${call}?;`];
|
|
246
|
+
},
|
|
247
|
+
helperCall: (helper, args) => {
|
|
248
|
+
used.helpers = true;
|
|
249
|
+
// the string code-point compare helpers take &str — borrow the owned String args.
|
|
250
|
+
const isStrCmp = helper === "strLt" || helper === "strLe" || helper === "strGt" || helper === "strGe";
|
|
251
|
+
const passed = isStrCmp ? args.map((a) => `&${a}`) : args;
|
|
252
|
+
// lenArr takes &[T] — borrow the owned Vec.
|
|
253
|
+
const finalArgs = helper === "lenArr" ? args.map((a) => `&${a}`) : passed;
|
|
254
|
+
return `bc_expr_${rustSnake(helper)}(${finalArgs.join(", ")})`;
|
|
255
|
+
},
|
|
256
|
+
binOp: (op, a, b) => `(${a} ${op} ${b})`,
|
|
257
|
+
concat: (parts) => {
|
|
258
|
+
// native String concat via a `[&str; N].concat()` — a string-literal part (`"…".to_string()`) is
|
|
259
|
+
// used as the bare `"…"` &str; any other owned String is borrowed with `.as_str()`. This shape is
|
|
260
|
+
// clippy-clean (no to_string_in_format_args / single_char_push_str / unnecessary_to_string).
|
|
261
|
+
const refs = parts.map((p) => {
|
|
262
|
+
const m = /^(".*")\.to_string\(\)$/.exec(p);
|
|
263
|
+
return m ? m[1] : `(${p}).as_str()`;
|
|
264
|
+
});
|
|
265
|
+
return `[${refs.join(", ")}].concat()`;
|
|
266
|
+
},
|
|
267
|
+
notOp: (a) => `(!${a})`,
|
|
268
|
+
structLit: (name, inits) => `${name} { ${inits.map((i) => `${rustFieldName(i.field)}: ${i.expr}`).join(", ")} }`,
|
|
269
|
+
arrLit: (elemTy, items) => `{ let __v: Vec<${elemTy}> = vec![${items.join(", ")}]; __v }`,
|
|
270
|
+
ternary: (cond, t, e, _ty) => `(if ${rustStripOuterParens(cond)} { ${rustStripOuterParens(t)} } else { ${rustStripOuterParens(e)} })`,
|
|
271
|
+
shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
|
|
272
|
+
// and: let temp = if left { <rStmts> rExpr } else { false }; or: if left { true } else { <rStmts> rExpr }.
|
|
273
|
+
// build a block that runs rStmts then yields rExpr (parens stripped — block-tail is not a sub-expr).
|
|
274
|
+
const rblock = `{ ${rStmts.join(" ")} ${rustStripOuterParens(rExpr)} }`;
|
|
275
|
+
if (op === "and") {
|
|
276
|
+
return [`let ${temp}: bool = if ${rustStripOuterParens(left)} ${rblock} else { false };`];
|
|
277
|
+
}
|
|
278
|
+
return [`let ${temp}: bool = if ${rustStripOuterParens(left)} { true } else ${rblock};`];
|
|
279
|
+
},
|
|
280
|
+
condGuard: (temp, ty, cond, tStmts, tExpr, eStmts, eExpr) => {
|
|
281
|
+
const tblock = `{ ${tStmts.join(" ")} ${rustStripOuterParens(tExpr)} }`;
|
|
282
|
+
const eblock = `{ ${eStmts.join(" ")} ${rustStripOuterParens(eExpr)} }`;
|
|
283
|
+
return [`let ${temp}: ${ty} = if ${rustStripOuterParens(cond)} ${tblock} else ${eblock};`];
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** camelCase helper name → snake_case for the Rust helper fn (addI64 → add_i64, strLt → str_lt). */
|
|
288
|
+
function rustSnake(name) {
|
|
289
|
+
return name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").replace(/([0-9])([a-z])/g, "$1_$2").toLowerCase();
|
|
290
|
+
}
|
|
291
|
+
/** strip ONE redundant outer paren pair from an expr (clippy `if (cond)` unused_parens). Only when the
|
|
292
|
+
* whole string is a single balanced `( … )` — a `(a) && (b)` stays untouched. */
|
|
293
|
+
function rustStripOuterParens(expr) {
|
|
294
|
+
if (!expr.startsWith("(") || !expr.endsWith(")"))
|
|
295
|
+
return expr;
|
|
296
|
+
let depth = 0;
|
|
297
|
+
for (let i = 0; i < expr.length; i++) {
|
|
298
|
+
if (expr[i] === "(")
|
|
299
|
+
depth++;
|
|
300
|
+
else if (expr[i] === ")") {
|
|
301
|
+
depth--;
|
|
302
|
+
if (depth === 0 && i !== expr.length - 1)
|
|
303
|
+
return expr; // closed before the end → not a single wrap
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return expr.slice(1, -1);
|
|
307
|
+
}
|
|
308
|
+
/** Rust float literal (finite; integral gets `.0`). */
|
|
309
|
+
function rustFloatLitExpr(n) {
|
|
310
|
+
if (Number.isInteger(n))
|
|
311
|
+
return `${n}.0`;
|
|
312
|
+
return String(n);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* emitExprHelpersRust — the FIXED runtime-free native-expr helper library (Rust). Emitted once per
|
|
316
|
+
* covered module that uses cond/guard/arith. Each helper is BYTE-EQUAL to expr-eval.ts core. Failures
|
|
317
|
+
* are the LOCAL BehaviorError with the SAME code strings as run_behavior.
|
|
318
|
+
*/
|
|
319
|
+
function emitExprHelpersRust() {
|
|
320
|
+
return `// ── native-expr runtime-free helper library (#108) — byte-equal to expr-eval.ts core ──
|
|
321
|
+
#[allow(dead_code)]
|
|
322
|
+
fn bc_expr_add_i64(a: i64, b: i64) -> Result<i64, BehaviorError> {
|
|
323
|
+
a.checked_add(b).ok_or_else(|| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
|
|
324
|
+
}
|
|
325
|
+
#[allow(dead_code)]
|
|
326
|
+
fn bc_expr_sub_i64(a: i64, b: i64) -> Result<i64, BehaviorError> {
|
|
327
|
+
a.checked_sub(b).ok_or_else(|| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
|
|
328
|
+
}
|
|
329
|
+
#[allow(dead_code)]
|
|
330
|
+
fn bc_expr_mul_i64(a: i64, b: i64) -> Result<i64, BehaviorError> {
|
|
331
|
+
a.checked_mul(b).ok_or_else(|| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
|
|
332
|
+
}
|
|
333
|
+
#[allow(dead_code)]
|
|
334
|
+
fn bc_expr_neg_i64(a: i64) -> Result<i64, BehaviorError> {
|
|
335
|
+
a.checked_neg().ok_or_else(|| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
|
|
336
|
+
}
|
|
337
|
+
#[allow(dead_code)]
|
|
338
|
+
fn bc_expr_check_finite(v: f64) -> Result<f64, BehaviorError> {
|
|
339
|
+
if v.is_finite() { Ok(v) } else { Err(BehaviorError::new("NAN_OR_INF", "non-finite float")) }
|
|
340
|
+
}
|
|
341
|
+
#[allow(dead_code)]
|
|
342
|
+
fn bc_expr_add_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a + b) }
|
|
343
|
+
#[allow(dead_code)]
|
|
344
|
+
fn bc_expr_sub_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a - b) }
|
|
345
|
+
#[allow(dead_code)]
|
|
346
|
+
fn bc_expr_mul_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a * b) }
|
|
347
|
+
#[allow(dead_code)]
|
|
348
|
+
fn bc_expr_neg_f(a: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(-a) }
|
|
349
|
+
#[allow(dead_code)]
|
|
350
|
+
fn bc_expr_mod_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a % b) }
|
|
351
|
+
#[allow(dead_code)]
|
|
352
|
+
fn bc_expr_mod_i64(a: i64, b: i64) -> Result<i64, BehaviorError> {
|
|
353
|
+
if b == 0 { return Err(BehaviorError::new("MOD_ZERO", "int mod by zero")); }
|
|
354
|
+
// Rust % is truncated (sign of dividend) — matches modCore. MIN % -1 would panic; guard it.
|
|
355
|
+
if a == i64::MIN && b == -1 { return Ok(0); }
|
|
356
|
+
Ok(a % b)
|
|
357
|
+
}
|
|
358
|
+
#[allow(dead_code)]
|
|
359
|
+
fn bc_expr_widen_i64f(a: i64) -> Result<f64, BehaviorError> {
|
|
360
|
+
if !(-9007199254740992..=9007199254740992).contains(&a) {
|
|
361
|
+
return Err(BehaviorError::new("PRECISION_LOSS", "int exceeds exact float range (±2^53)"));
|
|
362
|
+
}
|
|
363
|
+
Ok(a as f64)
|
|
364
|
+
}
|
|
365
|
+
#[allow(dead_code)]
|
|
366
|
+
fn bc_expr_div_f(a: f64, b: f64) -> Result<f64, BehaviorError> { bc_expr_check_finite(a / b) }
|
|
367
|
+
#[allow(dead_code)]
|
|
368
|
+
fn bc_expr_len_arr<T>(a: &[T]) -> Result<i64, BehaviorError> {
|
|
369
|
+
i64::try_from(a.len()).map_err(|_| BehaviorError::new("INT_OVERFLOW", "i64 overflow"))
|
|
370
|
+
}
|
|
371
|
+
// code-point (char) order comparison — matches cmpCodePoints (char-by-char, NOT byte order).
|
|
372
|
+
#[allow(dead_code)]
|
|
373
|
+
fn bc_expr_cmp_cp(a: &str, b: &str) -> std::cmp::Ordering {
|
|
374
|
+
let mut ia = a.chars();
|
|
375
|
+
let mut ib = b.chars();
|
|
376
|
+
loop {
|
|
377
|
+
match (ia.next(), ib.next()) {
|
|
378
|
+
(None, None) => return std::cmp::Ordering::Equal,
|
|
379
|
+
(None, Some(_)) => return std::cmp::Ordering::Less,
|
|
380
|
+
(Some(_), None) => return std::cmp::Ordering::Greater,
|
|
381
|
+
(Some(ca), Some(cb)) => {
|
|
382
|
+
if ca != cb {
|
|
383
|
+
return if (ca as u32) < (cb as u32) { std::cmp::Ordering::Less } else { std::cmp::Ordering::Greater };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
#[allow(dead_code)]
|
|
390
|
+
fn bc_expr_str_lt(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) == std::cmp::Ordering::Less }
|
|
391
|
+
#[allow(dead_code)]
|
|
392
|
+
fn bc_expr_str_le(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) != std::cmp::Ordering::Greater }
|
|
393
|
+
#[allow(dead_code)]
|
|
394
|
+
fn bc_expr_str_gt(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) == std::cmp::Ordering::Greater }
|
|
395
|
+
#[allow(dead_code)]
|
|
396
|
+
fn bc_expr_str_ge(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) != std::cmp::Ordering::Less }`;
|
|
397
|
+
}
|
|
199
398
|
// ── CONCRETE per-node row structs + row→outType copiers (mirror the Go concrete contract) ──────────
|
|
200
399
|
//
|
|
201
400
|
// Each covered node has a CONCRETE row struct `RawRowNR<Comp><Node>` whose fields are the node's projected
|
|
@@ -209,6 +408,8 @@ pub struct ${batch} {
|
|
|
209
408
|
function emitRawRowStructs(comp, typedNodes, plan) {
|
|
210
409
|
const parts = [];
|
|
211
410
|
for (const n of comp.body) {
|
|
411
|
+
if ("cond" in n)
|
|
412
|
+
continue; // #108: a cond node has no handler-result row (pure Expression).
|
|
212
413
|
const ref = typedNodes.get(n.id);
|
|
213
414
|
if ("map" in n) {
|
|
214
415
|
const arrRef = ref;
|
|
@@ -291,12 +492,14 @@ function boundRustType(node, typedNodes, plan) {
|
|
|
291
492
|
* struct (per-element). This is the fully-concrete, runtime-free replacement for the NativeRawComponentExec
|
|
292
493
|
* seam.
|
|
293
494
|
*/
|
|
294
|
-
function emitHandlerTrait(comp, typedNodes, plan,
|
|
295
|
-
// bc#97: async I/O model
|
|
296
|
-
// async node_
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
|
|
495
|
+
function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
496
|
+
// bc#97 (increment 2): async I/O model is PER-TERMINAL-NODE. A node whose terminal handler is declared
|
|
497
|
+
// async gets an `async fn node_*` (the consumer's impl returns a future the runner awaits at the call
|
|
498
|
+
// site); a sync terminal stays plain `fn node_*` (called directly, no await). Mixed sync+async in one
|
|
499
|
+
// trait is fine. `async fn` needs NO external runtime to compile — the future is `async fn` sugar over
|
|
500
|
+
// std, so the covered module stays runtime-free either way. Uniform ioModel:'async' ⇒ every method
|
|
501
|
+
// async (byte-identical to increment-1); ioModel unset/'sync' ⇒ every method plain `fn` (pre-#97).
|
|
502
|
+
const fnKwFor = (nodeId) => (ap.nodeIsAsync(nodeId) ? "async fn" : "fn");
|
|
300
503
|
const lines = [];
|
|
301
504
|
lines.push(`// ${handlerTraitName(comp.name)} — the CONCRETE per-component handler seam: one typed method`);
|
|
302
505
|
lines.push(`// per covered node (native ports struct IN, concrete row struct OUT). No generic boxed-ports`);
|
|
@@ -304,7 +507,10 @@ function emitHandlerTrait(comp, typedNodes, plan, isAsync = false) {
|
|
|
304
507
|
lines.push(`// node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
|
|
305
508
|
lines.push(`pub trait ${handlerTraitName(comp.name)} {`);
|
|
306
509
|
for (const n of comp.body) {
|
|
510
|
+
if ("cond" in n)
|
|
511
|
+
continue; // #108: a cond node has no handler method (pure Expression).
|
|
307
512
|
const method = nodeMethodName(n.id);
|
|
513
|
+
const fnKw = fnKwFor(n.id);
|
|
308
514
|
const bt = boundRustType(n, typedNodes, plan);
|
|
309
515
|
if ("map" in n) {
|
|
310
516
|
const m = n.map;
|
|
@@ -368,8 +574,11 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
|
368
574
|
const elemRowGo = rawElemStructName(comp.name, n.id);
|
|
369
575
|
const intoRowRef = mapElemRowRef(n, arrRef, plan);
|
|
370
576
|
const inits = [];
|
|
371
|
-
|
|
372
|
-
|
|
577
|
+
// Copy scalars (i64/f64/bool) copy directly; String/named/arr/opt clone (clippy clone_on_copy clean).
|
|
578
|
+
for (const f of overFields) {
|
|
579
|
+
const isCopy = f.type.kind === "scalar" && (f.type.scalar === "int" || f.type.scalar === "float" || f.type.scalar === "bool");
|
|
580
|
+
inits.push(` ${rustFieldName(f.name)}: over.${rustFieldName(f.name)}${isCopy ? "" : ".clone()"},`);
|
|
581
|
+
}
|
|
373
582
|
// move the into field off the concrete element row (named → struct literal; scalar/arr/opt → .val).
|
|
374
583
|
const intoValue = intoRowRef.kind === "named"
|
|
375
584
|
? `${intoRowRef.name} { ${findDecl(plan, intoRowRef.name).fields.map((f) => `${rustFieldName(f.name)}: into.${rustFieldName(f.name)}`).join(", ")} }`
|
|
@@ -385,17 +594,33 @@ ${inits.join("\n")}
|
|
|
385
594
|
}
|
|
386
595
|
return parts.join("\n\n");
|
|
387
596
|
}
|
|
388
|
-
/**
|
|
389
|
-
|
|
597
|
+
/**
|
|
598
|
+
* mapOverElemInfo — resolve a covered map's `over` collection element TypeRef + the OWNED Rust
|
|
599
|
+
* expression that yields the over Vec (#108, rust twin): prior-node arr field (cell clone) or an input
|
|
600
|
+
* array port with elemType (`in_.<field>.clone()`). Fail-closed for an input array without elemType.
|
|
601
|
+
*/
|
|
602
|
+
function mapOverElemInfo(comp, node, typedNodes, plan) {
|
|
390
603
|
const m = node.map;
|
|
391
604
|
const e = classifyExpr(m.over);
|
|
392
605
|
if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
|
|
393
606
|
const baseRef = typedNodes.get(e.path[0]);
|
|
394
607
|
const { ref } = typedFieldAccess(`${typedCell(e.path[0])}.borrow()`, baseRef, e.path.slice(1), plan);
|
|
395
|
-
if (ref.kind === "arr")
|
|
396
|
-
|
|
608
|
+
if (ref.kind === "arr") {
|
|
609
|
+
const fieldPath = e.path.slice(1).map((p) => `.${rustFieldName(p)}`).join("");
|
|
610
|
+
return { elemRef: ref.elem, overExpr: `${typedCell(e.path[0])}.borrow()${fieldPath}.clone()` };
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (e.kind === "ref" && e.path.length === 1) {
|
|
614
|
+
const schema = comp.inputPorts?.[e.path[0]];
|
|
615
|
+
const et = schema?.elemType;
|
|
616
|
+
if (et !== undefined)
|
|
617
|
+
return { elemRef: deriveTypeRef(et, plan), overExpr: `in_.${rustFieldName(e.path[0])}.clone()` };
|
|
397
618
|
}
|
|
398
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined map (#
|
|
619
|
+
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).`);
|
|
620
|
+
}
|
|
621
|
+
/** The Rust type of the over-element (back-compat shim). */
|
|
622
|
+
function mapOverElemType(comp, node, typedNodes, plan) {
|
|
623
|
+
return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
|
|
399
624
|
}
|
|
400
625
|
// ── eligibility ─────────────────────────────────────────────────────────────────────
|
|
401
626
|
function reconstructR(e) {
|
|
@@ -489,19 +714,30 @@ function outputIsNativeLowerable(comp) {
|
|
|
489
714
|
/**
|
|
490
715
|
* coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet), rust twin of the go predicate.
|
|
491
716
|
*/
|
|
492
|
-
function coveredMapNode(n, bodyIds) {
|
|
717
|
+
function coveredMapNode(n, bodyIds, comp) {
|
|
493
718
|
if (n === null || typeof n !== "object" || !("map" in n))
|
|
494
719
|
return false;
|
|
495
720
|
const m = n.map;
|
|
496
|
-
|
|
721
|
+
// #108 (rust twin of the go predicate): a guarded map (`when`) IS covered with NO `into` (guard+into
|
|
722
|
+
// is heterogeneous — fail-closed). over element type must be statically resolvable: prior-node arr
|
|
723
|
+
// field, or an input array port carrying `elemType` (BC does not infer — consumer-interface C3).
|
|
724
|
+
if (m.when !== undefined && m.into !== undefined)
|
|
497
725
|
return false;
|
|
498
726
|
if (m.policy !== undefined && m.policy !== "fail")
|
|
499
727
|
return false;
|
|
500
728
|
if (m.relationKind !== undefined && m.relationKind !== "single" && m.relationKind !== "connection")
|
|
501
729
|
return false;
|
|
502
730
|
const overE = classifyExpr(m.over);
|
|
503
|
-
if (overE.kind !== "ref"
|
|
731
|
+
if (overE.kind !== "ref")
|
|
504
732
|
return false;
|
|
733
|
+
const head = overE.path[0];
|
|
734
|
+
if (!bodyIds.has(head)) {
|
|
735
|
+
if (overE.path.length !== 1)
|
|
736
|
+
return false;
|
|
737
|
+
const schema = comp.inputPorts?.[head];
|
|
738
|
+
if (schema === undefined || schema.elemType === undefined)
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
505
741
|
const ports = m.ports;
|
|
506
742
|
for (const p of Object.keys(ports))
|
|
507
743
|
if (!portIsStatic(ports[p]))
|
|
@@ -533,11 +769,16 @@ function coverageRejectReason(comp) {
|
|
|
533
769
|
}
|
|
534
770
|
const bodyIds = new Set(comp.body.map((b) => b.id));
|
|
535
771
|
for (const n of comp.body) {
|
|
536
|
-
if ("cond" in n)
|
|
537
|
-
|
|
772
|
+
if ("cond" in n) {
|
|
773
|
+
// #108: a cond node is native-covered (its if/then/else compile via the runtime-free
|
|
774
|
+
// NativeExprCompiler; an uncovered expr fails closed at emit). cond requires outType.
|
|
775
|
+
if (n.outType === undefined)
|
|
776
|
+
return `node '${n.id}' cond has no outType (required to de-box the branch join)`;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
538
779
|
if ("map" in n) {
|
|
539
|
-
if (!coveredMapNode(n, bodyIds))
|
|
540
|
-
return `node '${n.id}' is a non-covered map shape (
|
|
780
|
+
if (!coveredMapNode(n, bodyIds, comp))
|
|
781
|
+
return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
|
|
541
782
|
continue;
|
|
542
783
|
}
|
|
543
784
|
if (!("component" in n))
|
|
@@ -576,8 +817,8 @@ function assertTypedCoverage(comp) {
|
|
|
576
817
|
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
818
|
}
|
|
578
819
|
// ── 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) {
|
|
820
|
+
/** Rust type for an input port's declared type (scalar → concrete; array+elemType → Vec<ElemT>; else Value). */
|
|
821
|
+
function inputPortRustType(schema, plan) {
|
|
581
822
|
switch (schema?.type) {
|
|
582
823
|
case "string":
|
|
583
824
|
return "String";
|
|
@@ -587,6 +828,14 @@ function inputPortRustType(schema) {
|
|
|
587
828
|
return "f64";
|
|
588
829
|
case "bool":
|
|
589
830
|
return "bool";
|
|
831
|
+
case "array":
|
|
832
|
+
case "arr": {
|
|
833
|
+
// #108: an input ARRAY port with a declared elemType lowers to a native Vec<ElemT>.
|
|
834
|
+
const et = schema.elemType;
|
|
835
|
+
if (et !== undefined && plan !== undefined)
|
|
836
|
+
return `Vec<${renderTypeRef(deriveTypeRef(et, plan))}>`;
|
|
837
|
+
return "Value";
|
|
838
|
+
}
|
|
590
839
|
default:
|
|
591
840
|
return "Value";
|
|
592
841
|
}
|
|
@@ -608,14 +857,14 @@ function inputScalarKind(schema) {
|
|
|
608
857
|
}
|
|
609
858
|
/** emitInStruct — the concrete input struct declaration for a covered component (fields = inputPorts).
|
|
610
859
|
* Empty inputPorts => empty struct. Derives Default so a consumer/test can build it field-by-field. */
|
|
611
|
-
function emitInStruct(comp) {
|
|
860
|
+
function emitInStruct(comp, plan) {
|
|
612
861
|
const name = inStructName(comp.name);
|
|
613
862
|
const ports = Object.keys(comp.inputPorts ?? {});
|
|
614
863
|
if (ports.length === 0) {
|
|
615
864
|
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\n#[derive(Default)]\npub struct ${name};`;
|
|
616
865
|
}
|
|
617
866
|
const fields = ports
|
|
618
|
-
.map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p])}, // ${JSON.stringify(p)}`)
|
|
867
|
+
.map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p], plan)}, // ${JSON.stringify(p)}`)
|
|
619
868
|
.join("\n");
|
|
620
869
|
return `// ${name} — the CONCRETE input for '${comp.name}' (fields = inputPorts; typed, consumer-built —
|
|
621
870
|
// NO generic Value slice, NO per-field boxing crosses the covered read boundary).
|
|
@@ -765,18 +1014,21 @@ function runnerResolveRef(priorNodeAt, atPos, typedNodes, plan, inputPorts) {
|
|
|
765
1014
|
function rustScalar(s) {
|
|
766
1015
|
return s === "string" ? "String" : s === "int" ? "i64" : s === "float" ? "f64" : "bool";
|
|
767
1016
|
}
|
|
768
|
-
function emitNativeRawRunner(comp, plan,
|
|
1017
|
+
function emitNativeRawRunner(comp, plan, ap) {
|
|
769
1018
|
const ep = execPlan(comp);
|
|
770
1019
|
const order = ep.order;
|
|
771
1020
|
const hasConcurrency = ep.parallelStageOf.size > 0;
|
|
772
1021
|
const fn = sanitize(comp.name);
|
|
773
|
-
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
// a
|
|
777
|
-
//
|
|
1022
|
+
const isAsync = ap.runnerIsAsync;
|
|
1023
|
+
// bc#97: the async runner awaits each async handler at its call site (derived per-node). A real-
|
|
1024
|
+
// concurrency stage (bc#87) dispatches handlers from scoped std threads — that mechanism cannot host
|
|
1025
|
+
// `.await` (a std thread is not an async task). Fail closed LOUDLY (rather than emit code that won't
|
|
1026
|
+
// compile) iff the runner is async AND has a concurrency stage. NOTE: with per-node granularity a
|
|
1027
|
+
// consumer CAN keep a concurrency-stage component sync (declare only its OTHER terminals async) — only
|
|
1028
|
+
// a component that both concurrency-stages AND awaits is rejected. Async + concurrent-stage (issuing
|
|
1029
|
+
// the stage's futures with join!) is a documented follow-up.
|
|
778
1030
|
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
|
|
1031
|
+
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
1032
|
}
|
|
781
1033
|
const typedNodes = new Map();
|
|
782
1034
|
for (const n of comp.body)
|
|
@@ -797,39 +1049,44 @@ function emitNativeRawRunner(comp, plan, isAsync = false) {
|
|
|
797
1049
|
};
|
|
798
1050
|
const outRef = comp.outputType !== undefined ? deriveTypeRef(comp.outputType, plan) : undefined;
|
|
799
1051
|
const outTy = outRef ? renderTypeRef(outRef) : "Value";
|
|
800
|
-
// Does any
|
|
1052
|
+
// Does any expression read an input param? If not, `in_` is unused — bind `_in_` to stay clippy-clean.
|
|
1053
|
+
// #108: this must recurse ALL expression operators (cond if/then/else, map when + ports, arith/compare/…),
|
|
1054
|
+
// not just ref/concat — a cond `if:{gt:[{add:[{ref:["x"]}…]}]}` reads `in_.x` through nested operators.
|
|
801
1055
|
const readsInput = (() => {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
if (
|
|
805
|
-
return
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1056
|
+
// walk any Expression IR node; `bound` = names bound in this scope (a map $as) that are NOT input.
|
|
1057
|
+
const walk = (node, atPos, bound) => {
|
|
1058
|
+
if (node === null || typeof node === "boolean" || typeof node === "string" || typeof node === "number")
|
|
1059
|
+
return false;
|
|
1060
|
+
if (Array.isArray(node))
|
|
1061
|
+
return node.some((el) => walk(el, atPos, bound));
|
|
1062
|
+
const rec = node;
|
|
1063
|
+
const keys = Object.keys(rec);
|
|
1064
|
+
if (keys.length === 1 && (keys[0] === "ref" || keys[0] === "refOpt")) {
|
|
1065
|
+
const path = rec[keys[0]];
|
|
1066
|
+
const head = path[0];
|
|
1067
|
+
return !bound.has(head) && !priorNodeAt(head, atPos);
|
|
1068
|
+
}
|
|
1069
|
+
// any other single-key operator (obj/arr/concat/add/gt/cond/…): recurse into every child value.
|
|
1070
|
+
return keys.some((k) => walk(rec[k], atPos, bound));
|
|
812
1071
|
};
|
|
813
1072
|
return [...order.keys()].some((k) => {
|
|
814
1073
|
const nd = comp.body[order[k]];
|
|
815
1074
|
if ("map" in nd) {
|
|
816
1075
|
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));
|
|
1076
|
+
const bound = new Set([m.as]);
|
|
1077
|
+
// the map's OVER is evaluated in the outer scope (may read input); ports + when in element scope.
|
|
1078
|
+
if (walk(m.over, k, new Set()))
|
|
1079
|
+
return true;
|
|
1080
|
+
if (m.when !== undefined && walk(m.when, k, bound))
|
|
1081
|
+
return true;
|
|
1082
|
+
return Object.values(m.ports).some((pn) => walk(pn, k, bound));
|
|
1083
|
+
}
|
|
1084
|
+
if ("cond" in nd) {
|
|
1085
|
+
const c = nd.cond;
|
|
1086
|
+
return walk(c.if, k, new Set()) || walk(c.then, k, new Set()) || walk(c.else, k, new Set());
|
|
830
1087
|
}
|
|
831
1088
|
const cr = nd;
|
|
832
|
-
return Object.values(cr.ports).some((pn) => walk(pn, k));
|
|
1089
|
+
return Object.values(cr.ports).some((pn) => walk(pn, k, new Set()));
|
|
833
1090
|
});
|
|
834
1091
|
})();
|
|
835
1092
|
const inParam = readsInput ? "in_" : "_in_";
|
|
@@ -853,8 +1110,10 @@ function emitNativeRawRunner(comp, plan, isAsync = false) {
|
|
|
853
1110
|
// so the runner is itself `async fn` and returns a future the (already-async) consumer awaits. Sync
|
|
854
1111
|
// default emits `pub fn` exactly as before (byte-identical).
|
|
855
1112
|
const fnKw = isAsync ? "pub async fn" : "pub fn";
|
|
1113
|
+
// #108: a component whose body is ALL cond nodes dispatches NO handler — bind `_handlers` (clippy-clean).
|
|
1114
|
+
const dispatchesHandler = comp.body.some((n) => "map" in n || "component" in n);
|
|
856
1115
|
lines.push(`${fnKw} run_native_raw_struct_${fn}${bound}(`);
|
|
857
|
-
lines.push(` handlers: &H,`);
|
|
1116
|
+
lines.push(` ${dispatchesHandler ? "handlers" : "_handlers"}: &H,`);
|
|
858
1117
|
lines.push(` ${inParam}: ${inStructName(comp.name)},`);
|
|
859
1118
|
lines.push(`) -> Result<${outTy}, BehaviorError> {`);
|
|
860
1119
|
for (const k of order.keys()) {
|
|
@@ -872,7 +1131,12 @@ function emitNativeRawRunner(comp, plan, isAsync = false) {
|
|
|
872
1131
|
continue;
|
|
873
1132
|
}
|
|
874
1133
|
if ("map" in comp.body[i]) {
|
|
875
|
-
lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt,
|
|
1134
|
+
lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, ap.nodeIsAsync(comp.body[i].id)));
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
// #108: a cond node — pure Expression (no handler). Compile via the runtime-free NativeExprCompiler.
|
|
1138
|
+
if ("cond" in comp.body[i]) {
|
|
1139
|
+
lines.push(emitCondArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt));
|
|
876
1140
|
continue;
|
|
877
1141
|
}
|
|
878
1142
|
const node = comp.body[i];
|
|
@@ -918,7 +1182,10 @@ function emitNativeRawRunner(comp, plan, isAsync = false) {
|
|
|
918
1182
|
inits.push(emitPortInit(pn, expr, ty, resolveRef));
|
|
919
1183
|
}
|
|
920
1184
|
const boundArg = node.bindField !== undefined ? `bound_${s}` : "None";
|
|
921
|
-
|
|
1185
|
+
// bc#97: derived per-node await — this point-read's future is awaited HERE (its result-consumption
|
|
1186
|
+
// point) iff THIS node's terminal handler is async. A sync handler in the same async runner is called
|
|
1187
|
+
// directly (no `.await`). The runner being `async fn` does not force every call site to await.
|
|
1188
|
+
const awaitSfx = ap.nodeIsAsync(node.id) ? ".await" : "";
|
|
922
1189
|
lines.push(`${indent}let ports_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
923
1190
|
lines.push(`${indent}let row_${s} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
|
|
924
1191
|
lines.push(`${indent} Some(r) => r,`);
|
|
@@ -1084,26 +1351,26 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1084
1351
|
* - #93 shape 2: NON-batched map...into — dispatch ONCE PER ELEMENT (N physical requests), into per element.
|
|
1085
1352
|
* - #93 shape 3: BATCHED map NO into — dispatch once, collect each element from the concrete element row.
|
|
1086
1353
|
*/
|
|
1087
|
-
function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt,
|
|
1354
|
+
function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt,
|
|
1355
|
+
// bc#97: whether THIS map node's terminal handler is async (derived per-node) — awaits the batched /
|
|
1356
|
+
// per-element handler call at its consumption point iff true. Sync map handlers are called directly.
|
|
1357
|
+
isAsync = false) {
|
|
1088
1358
|
const m = node.map;
|
|
1089
1359
|
const awaitSfx = isAsync ? ".await" : "";
|
|
1090
1360
|
const batched = m.batched === true;
|
|
1091
1361
|
const hasInto = m.into !== undefined;
|
|
1362
|
+
const hasGuard = m.when !== undefined;
|
|
1092
1363
|
const s = sanitize(node.id);
|
|
1093
1364
|
const structName = portsStructName(comp.name, node.id);
|
|
1094
1365
|
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("") : "";
|
|
1366
|
+
// #108: over element type + the OWNED over-Vec expression (prior-node arr OR input array w/ elemType).
|
|
1367
|
+
const { elemRef: overElemRef, overExpr } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
1101
1368
|
const elemMat = mapElemMaterializerName(node.id);
|
|
1102
1369
|
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1103
1370
|
const asName = m.as;
|
|
1104
1371
|
const portNames = Object.keys(m.ports);
|
|
1105
1372
|
const elemArrRef = typedNodes.get(node.id);
|
|
1106
|
-
const elemName = elemArrRef.elem
|
|
1373
|
+
const elemName = renderTypeRef(elemArrRef.elem);
|
|
1107
1374
|
const resolveRef = (head, restPath, opt) => {
|
|
1108
1375
|
if (opt)
|
|
1109
1376
|
return null;
|
|
@@ -1138,37 +1405,77 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync =
|
|
|
1138
1405
|
}
|
|
1139
1406
|
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1140
1407
|
return null;
|
|
1141
|
-
|
|
1408
|
+
// OWNED value. When restPath is EMPTY, `expr` is a bare REFERENCE (`oel_${s}` = &Elem-scalar, or
|
|
1409
|
+
// `cell.borrow()` = Ref<scalar>) — deref with `*` (Copy) / `.clone()` via deref (String). When a
|
|
1410
|
+
// `.field` was appended, `expr` is a value PLACE (auto-deref) — read it directly (`.clone()` String).
|
|
1411
|
+
const isBareRef = restPath.length === 0;
|
|
1412
|
+
let owned;
|
|
1413
|
+
if (acc.scalar === "string") {
|
|
1414
|
+
owned = isBareRef ? `(*${expr}).clone()` : `${expr}.clone()`;
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
owned = isBareRef ? `*${expr}` : expr;
|
|
1418
|
+
}
|
|
1142
1419
|
return { expr: owned, scalar: rustScalar(acc.scalar) };
|
|
1143
1420
|
};
|
|
1144
1421
|
// build the per-element CONCRETE native ports struct. `il` = indent inside the element loop.
|
|
1145
1422
|
const buildPorts = (il) => {
|
|
1146
1423
|
const out = [];
|
|
1147
1424
|
const inits = [];
|
|
1425
|
+
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1148
1426
|
for (const pn of portNames) {
|
|
1149
|
-
const ty = portFieldRustType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'
|
|
1427
|
+
const ty = portFieldRustType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding);
|
|
1150
1428
|
inits.push(emitPortInit(pn, m.ports[pn], ty, resolveRef));
|
|
1151
1429
|
}
|
|
1152
1430
|
out.push(`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
1153
1431
|
return out;
|
|
1154
1432
|
};
|
|
1433
|
+
// #108 guard: compile `when` to a native bool over an element-scoped resolveHead ($as element value,
|
|
1434
|
+
// prior node, input port). On skip: `continue` (element excluded — matches run_behavior's keep-gate).
|
|
1435
|
+
const emitGuard = (il) => {
|
|
1436
|
+
if (!hasGuard)
|
|
1437
|
+
return [];
|
|
1438
|
+
const resolveHead = (head) => {
|
|
1439
|
+
if (head === asName)
|
|
1440
|
+
return { expr: `oel_${s}.clone()`, ref: overElemRef };
|
|
1441
|
+
if (priorHere(head))
|
|
1442
|
+
return { expr: `${typedCell(head)}.borrow().clone()`, ref: typedNodes.get(head) };
|
|
1443
|
+
return rustInputHeadRef(head, comp, plan);
|
|
1444
|
+
};
|
|
1445
|
+
const g = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compileBool(m.when);
|
|
1446
|
+
const out = [];
|
|
1447
|
+
for (const st of g.stmts)
|
|
1448
|
+
out.push(`${il}${st}`);
|
|
1449
|
+
// bind the keep flag to a var so clippy's nonminimal_bool doesn't rewrite `!(a > b)` (the `when`
|
|
1450
|
+
// expr must stay verbatim = run_behavior's `{cond:[when,true,false]}` strict-bool keep-gate).
|
|
1451
|
+
out.push(`${il}let __keep = ${rustStripOuterParens(g.expr)};`);
|
|
1452
|
+
out.push(`${il}if !__keep { continue; }`);
|
|
1453
|
+
return out;
|
|
1454
|
+
};
|
|
1155
1455
|
const lines = [];
|
|
1156
1456
|
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 : ""}) ──`);
|
|
1457
|
+
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
1458
|
let indent = " ";
|
|
1159
1459
|
if (m.parent !== undefined && priorNodeAt(m.parent, atPos)) {
|
|
1160
1460
|
lines.push(`${indent}if produced_${sanitize(m.parent)}.get() {`);
|
|
1161
1461
|
closers.unshift(`${indent}}`);
|
|
1162
1462
|
indent += " ";
|
|
1163
1463
|
}
|
|
1164
|
-
lines.push(`${indent}let over_${s} = ${
|
|
1464
|
+
lines.push(`${indent}let over_${s} = ${overExpr};`);
|
|
1165
1465
|
lines.push(`${indent}let mut built_${s}: Vec<${elemName}> = Vec::with_capacity(over_${s}.len());`);
|
|
1166
1466
|
if (batched) {
|
|
1467
|
+
// build kept-element ports (+ kept over elements for into). Batched into is never guarded (rejected).
|
|
1167
1468
|
lines.push(`${indent}let mut items_${s}: Vec<${structName}> = Vec::with_capacity(over_${s}.len());`);
|
|
1469
|
+
if (hasInto)
|
|
1470
|
+
lines.push(`${indent}let mut kept_${s}: Vec<${renderTypeRef(overElemRef)}> = Vec::with_capacity(over_${s}.len());`);
|
|
1168
1471
|
lines.push(`${indent}for oel_${s} in over_${s}.iter() {`);
|
|
1472
|
+
for (const l of emitGuard(indent + " "))
|
|
1473
|
+
lines.push(l);
|
|
1169
1474
|
for (const l of buildPorts(indent + " "))
|
|
1170
1475
|
lines.push(l);
|
|
1171
1476
|
lines.push(`${indent} items_${s}.push(ep_${s});`);
|
|
1477
|
+
if (hasInto)
|
|
1478
|
+
lines.push(`${indent} kept_${s}.push(oel_${s}.clone());`);
|
|
1172
1479
|
lines.push(`${indent}}`);
|
|
1173
1480
|
lines.push(`${indent}if !items_${s}.is_empty() {`);
|
|
1174
1481
|
lines.push(`${indent} let want_${s} = items_${s}.len();`);
|
|
@@ -1183,23 +1490,28 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync =
|
|
|
1183
1490
|
lines.push(`${indent} if row_${s}.rows.len() != want_${s} {`);
|
|
1184
1491
|
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
1492
|
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
1493
|
if (hasInto) {
|
|
1494
|
+
// zip the aligned batch rows onto each KEPT over element (into materializer).
|
|
1495
|
+
lines.push(`${indent} let mut rows_iter_${s} = row_${s}.rows.into_iter();`);
|
|
1496
|
+
lines.push(`${indent} for over_el_${s} in kept_${s}.iter() {`);
|
|
1497
|
+
lines.push(`${indent} let er_${s} = rows_iter_${s}.next().expect("aligned batch row");`);
|
|
1190
1498
|
lines.push(`${indent} built_${s}.push(${elemMat}(over_el_${s}, er_${s}));`);
|
|
1499
|
+
lines.push(`${indent} }`);
|
|
1191
1500
|
}
|
|
1192
1501
|
else {
|
|
1193
|
-
|
|
1502
|
+
// no-into: collect each element DIRECTLY from the aligned per-element row (result = kept rows).
|
|
1503
|
+
lines.push(`${indent} for er_${s} in row_${s}.rows.into_iter() {`);
|
|
1194
1504
|
lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
|
|
1195
1505
|
lines.push(`${indent} built_${s}.push(el_${s});`);
|
|
1506
|
+
lines.push(`${indent} }`);
|
|
1196
1507
|
}
|
|
1197
|
-
lines.push(`${indent} }`);
|
|
1198
1508
|
lines.push(`${indent}}`);
|
|
1199
1509
|
}
|
|
1200
1510
|
else {
|
|
1201
|
-
// #93 shape 2: NON-batched map
|
|
1511
|
+
// #93 shape 2 / #108: NON-batched map — dispatch ONCE PER KEPT ELEMENT.
|
|
1202
1512
|
lines.push(`${indent}for oel_${s} in over_${s}.iter() {`);
|
|
1513
|
+
for (const l of emitGuard(indent + " "))
|
|
1514
|
+
lines.push(l);
|
|
1203
1515
|
for (const l of buildPorts(indent + " "))
|
|
1204
1516
|
lines.push(l);
|
|
1205
1517
|
lines.push(`${indent} let er_${s} = match handlers.${nodeMethodName(node.id)}(&ep_${s}, None)${awaitSfx} {`);
|
|
@@ -1213,6 +1525,7 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync =
|
|
|
1213
1525
|
lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s}, er_${s}));`);
|
|
1214
1526
|
}
|
|
1215
1527
|
else {
|
|
1528
|
+
lines.push(`${indent} let _ = &oel_${s};`);
|
|
1216
1529
|
lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
|
|
1217
1530
|
lines.push(`${indent} built_${s}.push(el_${s});`);
|
|
1218
1531
|
}
|
|
@@ -1224,6 +1537,71 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync =
|
|
|
1224
1537
|
lines.push(c);
|
|
1225
1538
|
return lines.join("\n");
|
|
1226
1539
|
}
|
|
1540
|
+
/** #108: resolve an INPUT port head for a native-expr ref (rust). Scalar → `in_.<field>` (owned: String
|
|
1541
|
+
* cloned, Copy as-is); array WITH elemType → `in_.<field>.clone()` of Vec<ElemT> (for `len`); else null. */
|
|
1542
|
+
function rustInputHeadRef(head, comp, plan) {
|
|
1543
|
+
const schema = comp.inputPorts?.[head];
|
|
1544
|
+
if (schema === undefined)
|
|
1545
|
+
return null;
|
|
1546
|
+
const sc = inputScalarKind(schema);
|
|
1547
|
+
if (sc !== undefined) {
|
|
1548
|
+
const scalarRef = { kind: "scalar", scalar: sc === "i64" ? "int" : sc === "f64" ? "float" : sc === "String" ? "string" : "bool" };
|
|
1549
|
+
const field = `in_.${rustFieldName(head)}`;
|
|
1550
|
+
return { expr: sc === "String" ? `${field}.clone()` : field, ref: scalarRef };
|
|
1551
|
+
}
|
|
1552
|
+
const et = schema.elemType;
|
|
1553
|
+
if ((schema.type === "array" || schema.type === "arr") && et !== undefined) {
|
|
1554
|
+
// no clone: the only array use inside a cond/guard expr is `len`, which BORROWS (`&in_.xs`).
|
|
1555
|
+
return { expr: `in_.${rustFieldName(head)}`, ref: { kind: "arr", elem: deriveTypeRef(et, plan) } };
|
|
1556
|
+
}
|
|
1557
|
+
return null;
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* emitCondArm — the struct-native exec of a covered cond node (#108, rust). Mirrors run_behavior's cond
|
|
1561
|
+
* lower: compile `if` to a native bool, materialize the TAKEN branch to the node's outType (only the
|
|
1562
|
+
* taken branch evaluated), assign the node's typed cell + set produced. NO handler dispatch. Compiles
|
|
1563
|
+
* via the runtime-free NativeExprCompiler (byte-equal to `evaluate`); an uncovered expr fails closed.
|
|
1564
|
+
*/
|
|
1565
|
+
function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
|
|
1566
|
+
const c = node.cond;
|
|
1567
|
+
const s = sanitize(node.id);
|
|
1568
|
+
const nodeRef = typedNodes.get(node.id);
|
|
1569
|
+
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1570
|
+
const resolveHead = (head) => {
|
|
1571
|
+
if (priorHere(head))
|
|
1572
|
+
return { expr: `${typedCell(head)}.borrow().clone()`, ref: typedNodes.get(head) };
|
|
1573
|
+
return rustInputHeadRef(head, comp, plan);
|
|
1574
|
+
};
|
|
1575
|
+
const compiler = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan));
|
|
1576
|
+
const resolveDecl = (name) => findDecl(plan, name).fields;
|
|
1577
|
+
const cond = compiler.compileBool(c.if);
|
|
1578
|
+
const thenC = compiler.compileTo(c.then, nodeRef, resolveDecl);
|
|
1579
|
+
const elseC = compiler.compileTo(c.else, nodeRef, resolveDecl);
|
|
1580
|
+
const lines = [];
|
|
1581
|
+
lines.push(` // ── cond '${node.id}'${c.parent ? " (parent:" + c.parent + ")" : ""} — pure Expression (no handler) ──`);
|
|
1582
|
+
let indent = " ";
|
|
1583
|
+
const closers = [];
|
|
1584
|
+
if (c.parent !== undefined && priorNodeAt(c.parent, atPos)) {
|
|
1585
|
+
lines.push(`${indent}if produced_${sanitize(c.parent)}.get() {`);
|
|
1586
|
+
closers.unshift(`${indent}}`);
|
|
1587
|
+
indent += " ";
|
|
1588
|
+
}
|
|
1589
|
+
for (const st of cond.stmts)
|
|
1590
|
+
lines.push(`${indent}${st}`);
|
|
1591
|
+
lines.push(`${indent}if ${rustStripOuterParens(cond.expr)} {`);
|
|
1592
|
+
for (const st of thenC.stmts)
|
|
1593
|
+
lines.push(`${indent} ${st}`);
|
|
1594
|
+
lines.push(`${indent} *${typedCell(node.id)}.borrow_mut() = ${thenC.expr};`);
|
|
1595
|
+
lines.push(`${indent}} else {`);
|
|
1596
|
+
for (const st of elseC.stmts)
|
|
1597
|
+
lines.push(`${indent} ${st}`);
|
|
1598
|
+
lines.push(`${indent} *${typedCell(node.id)}.borrow_mut() = ${elseC.expr};`);
|
|
1599
|
+
lines.push(`${indent}}`);
|
|
1600
|
+
lines.push(`${indent}produced_${s}.set(true);`);
|
|
1601
|
+
for (const cl of closers)
|
|
1602
|
+
lines.push(cl);
|
|
1603
|
+
return lines.join("\n");
|
|
1604
|
+
}
|
|
1227
1605
|
/** emitElemMove — move a concrete element row (RawElemNR, error already handled) into a fresh `let` local
|
|
1228
1606
|
* of the element outType. Named → struct literal from moved fields; scalar/arr/opt → .val. */
|
|
1229
1607
|
function emitElemMove(letBind, rowExpr, ref, plan) {
|
|
@@ -1306,8 +1684,11 @@ fn unknown_component(component: &str) -> BehaviorError {
|
|
|
1306
1684
|
}`;
|
|
1307
1685
|
// ── module assembly ──────────────────────────────────────────────────────────────────
|
|
1308
1686
|
function emit(ctx) {
|
|
1309
|
-
// bc#97
|
|
1310
|
-
|
|
1687
|
+
// bc#97 (increment 2): the SSoT language-agnostic propagation pass derives, PER COMPONENT + PER NODE,
|
|
1688
|
+
// which handler-call boundaries must be awaited (bottom-up over the declared async terminal handlers).
|
|
1689
|
+
// rust CONSUMES this annotation (async fn + .await where derived); go ignores it. per-node granularity
|
|
1690
|
+
// is a strict generalization of increment-1's uniform ioModel:'async' (= every terminal async).
|
|
1691
|
+
const asyncPlans = buildAsyncPlans(ctx.ir, ctx.ioModel);
|
|
1311
1692
|
for (const c of ctx.ir.components)
|
|
1312
1693
|
assertTypedCoverage(c);
|
|
1313
1694
|
const native = ctx.ir.components.filter(isNativeRaw);
|
|
@@ -1342,6 +1723,8 @@ ${emitStructSurface([])}
|
|
|
1342
1723
|
.replace(/^( {4})([a-z_][A-Za-z0-9_]*: )/gm, "$1pub $2")
|
|
1343
1724
|
.replace(/^( {4})(r#[a-z_][A-Za-z0-9_]*: )/gm, "$1pub $2");
|
|
1344
1725
|
const defaults = emitDefaults(plan);
|
|
1726
|
+
// #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler).
|
|
1727
|
+
rustExprUsed.helpers = false;
|
|
1345
1728
|
const structs = [];
|
|
1346
1729
|
const rowStructs = [];
|
|
1347
1730
|
const handlerTraits = [];
|
|
@@ -1349,21 +1732,26 @@ ${emitStructSurface([])}
|
|
|
1349
1732
|
const elemMaterializers = [];
|
|
1350
1733
|
for (const c of native) {
|
|
1351
1734
|
const typedNodes = new Map();
|
|
1735
|
+
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
1736
|
+
for (const n of c.body)
|
|
1737
|
+
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1352
1738
|
for (const n of c.body) {
|
|
1353
1739
|
if ("map" in n)
|
|
1354
|
-
structs.push(emitMapPortsStructs(c, n));
|
|
1740
|
+
structs.push(emitMapPortsStructs(c, n, typedNodes, plan));
|
|
1741
|
+
else if ("cond" in n) {
|
|
1742
|
+
// #108: a cond node has NO ports struct (pure Expression — no handler dispatch).
|
|
1743
|
+
}
|
|
1355
1744
|
else
|
|
1356
1745
|
structs.push(emitPortsStruct(c, n));
|
|
1357
|
-
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1358
1746
|
}
|
|
1359
1747
|
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
1360
|
-
handlerTraits.push(emitHandlerTrait(c, typedNodes, plan,
|
|
1361
|
-
inStructs.push(emitInStruct(c));
|
|
1748
|
+
handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
|
|
1749
|
+
inStructs.push(emitInStruct(c, plan));
|
|
1362
1750
|
const mm = emitMapElemMaterializers(c, typedNodes, plan);
|
|
1363
1751
|
if (mm)
|
|
1364
1752
|
elemMaterializers.push(mm);
|
|
1365
1753
|
}
|
|
1366
|
-
const runners = native.map((c) => emitNativeRawRunner(c, plan,
|
|
1754
|
+
const runners = native.map((c) => emitNativeRawRunner(c, plan, asyncPlans.get(c.name))).join("\n\n");
|
|
1367
1755
|
const structSurface = emitStructSurface(native);
|
|
1368
1756
|
// (nonNative.length === 0 is guaranteed here — the fail-closed guard above threw otherwise.)
|
|
1369
1757
|
const banner = `#![allow(dead_code, unused_imports, non_snake_case)]
|
|
@@ -1389,8 +1777,15 @@ use std::cell::RefCell;`;
|
|
|
1389
1777
|
decls || undefined,
|
|
1390
1778
|
defaults || undefined,
|
|
1391
1779
|
UNKNOWN_COMPONENT_HELPER,
|
|
1392
|
-
|
|
1393
|
-
|
|
1780
|
+
rustExprUsed.helpers
|
|
1781
|
+
? `// #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()}`
|
|
1782
|
+
: undefined,
|
|
1783
|
+
structs.some((r) => r.trim().length > 0)
|
|
1784
|
+
? `// 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")}`
|
|
1785
|
+
: undefined,
|
|
1786
|
+
rowStructs.some((r) => r.trim().length > 0)
|
|
1787
|
+
? `// CONCRETE per-node row structs (typed fields = the node's outType; + error signal).\n${rowStructs.filter((r) => r.trim().length > 0).join("\n\n")}`
|
|
1788
|
+
: undefined,
|
|
1394
1789
|
`// CONCRETE per-component input structs (fields = inputPorts).\n${inStructs.join("\n\n")}`,
|
|
1395
1790
|
`// CONCRETE per-component handler traits (one node_* method per node — native ports IN, row OUT).\n${handlerTraits.join("\n\n")}`,
|
|
1396
1791
|
elemMaterializers.length
|
|
@@ -1410,13 +1805,18 @@ use std::cell::RefCell;`;
|
|
|
1410
1805
|
* Value for the equivalence pin. Lives OUTSIDE the covered module so the module stays literal whole-file
|
|
1411
1806
|
* zero (the ser_* / decode helpers are test glue, never on the consumer's read hot path).
|
|
1412
1807
|
*/
|
|
1413
|
-
export function rustTypedNativeObserve(ir) {
|
|
1808
|
+
export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
1414
1809
|
const components = ir.components;
|
|
1415
1810
|
const native = components.filter(isNativeRaw);
|
|
1416
1811
|
const plan = buildTypePlan(ir);
|
|
1417
1812
|
const needSync = native.some((c) => concurrencyPlan(c) !== null);
|
|
1418
1813
|
const serializers = emitSerializers(plan);
|
|
1419
1814
|
const marshallers = emitMarshallers(plan);
|
|
1815
|
+
// bc#97: the observe glue must MATCH the covered module's per-node async-ness — the scripted handler's
|
|
1816
|
+
// node_* methods are `async fn` exactly where the trait's are, and observe_native_raw awaits an async
|
|
1817
|
+
// runner. Built from the SAME SSoT propagation pass the covered emitter used, so glue never drifts.
|
|
1818
|
+
const asyncPlans = buildAsyncPlans(ir, ioModel);
|
|
1819
|
+
const anyAsyncRunner = native.some((c) => asyncPlans.get(c.name).runnerIsAsync);
|
|
1420
1820
|
// ── per-component input decoders: generic &[(String,Value)] -> concrete InNR_<comp> (test glue). ──
|
|
1421
1821
|
const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
|
|
1422
1822
|
// ── concrete scripted handler: implements every HandlerNR<comp> trait by decoding a scripted Value
|
|
@@ -1425,12 +1825,18 @@ export function rustTypedNativeObserve(ir) {
|
|
|
1425
1825
|
const emittedDecode = new Set();
|
|
1426
1826
|
const traitImpls = [];
|
|
1427
1827
|
for (const c of native) {
|
|
1828
|
+
const ap = asyncPlans.get(c.name);
|
|
1829
|
+
// the scripted impl's node_* is `async fn` iff the trait's is (same per-node derivation).
|
|
1830
|
+
const fnKwFor = (nodeId) => (ap.nodeIsAsync(nodeId) ? "async fn" : "fn");
|
|
1428
1831
|
const typedNodes = new Map();
|
|
1429
1832
|
for (const n of c.body)
|
|
1430
1833
|
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1431
1834
|
const methods = [];
|
|
1432
1835
|
for (const n of c.body) {
|
|
1836
|
+
if ("cond" in n)
|
|
1837
|
+
continue; // #108: a cond node has no handler method (pure Expression) — no scripted impl.
|
|
1433
1838
|
const ref = typedNodes.get(n.id);
|
|
1839
|
+
const fnKw = fnKwFor(n.id);
|
|
1434
1840
|
// bc#90 / runtime-free: the scripted impl's `_bound` param mirrors the trait's typed produced-aware
|
|
1435
1841
|
// Option<K> (NOT a boxed Value) — computed per node so the impl signature matches the trait.
|
|
1436
1842
|
const bt = boundRustType(n, typedNodes, plan);
|
|
@@ -1445,7 +1851,7 @@ export function rustTypedNativeObserve(ir) {
|
|
|
1445
1851
|
const portsT = batched ? `${portsStructName(c.name, n.id)}Batch` : portsStructName(c.name, n.id);
|
|
1446
1852
|
const retT = batched ? batchT : elemT;
|
|
1447
1853
|
if (batched) {
|
|
1448
|
-
methods.push(`
|
|
1854
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
|
|
1449
1855
|
let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
|
|
1450
1856
|
if is_err {
|
|
1451
1857
|
return Some(${batchT} { is_error: true, err, ..Default::default() });
|
|
@@ -1456,7 +1862,7 @@ export function rustTypedNativeObserve(ir) {
|
|
|
1456
1862
|
}`);
|
|
1457
1863
|
}
|
|
1458
1864
|
else {
|
|
1459
|
-
methods.push(`
|
|
1865
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${elemT}> {
|
|
1460
1866
|
let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
|
|
1461
1867
|
if is_err {
|
|
1462
1868
|
return Some(${elemT} { is_error: true, err, ..Default::default() });
|
|
@@ -1471,7 +1877,7 @@ export function rustTypedNativeObserve(ir) {
|
|
|
1471
1877
|
const node = n;
|
|
1472
1878
|
const rowT = rawRowStructName(c.name, node.id);
|
|
1473
1879
|
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
1474
|
-
methods.push(`
|
|
1880
|
+
methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, _ports: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
|
|
1475
1881
|
let (is_err, err, ok) = self.next(${rustStrLit(node.component)})?;
|
|
1476
1882
|
if is_err {
|
|
1477
1883
|
return Some(${rowT} { is_error: true, err, ..Default::default() });
|
|
@@ -1490,7 +1896,12 @@ export function rustTypedNativeObserve(ir) {
|
|
|
1490
1896
|
// bridge it to the bc-runtime `behavior_contracts::BehaviorError` at the observe boundary (same
|
|
1491
1897
|
// code/message) so the conformance harness (main.rs) reads the stable code without the covered
|
|
1492
1898
|
// module ever touching bc runtime. decode_* is test glue and returns the local error too.
|
|
1493
|
-
|
|
1899
|
+
// bc#97: an async runner returns a Future — drive it to completion with the std-only block-on
|
|
1900
|
+
// (bc_block_on) so observe_native_raw keeps its SYNC external signature (the harness is unchanged).
|
|
1901
|
+
// The scripted async node_* return ready values, so the future needs no real executor / no runtime.
|
|
1902
|
+
const runnerCall = `super::run_native_raw_struct_${sanitize(c.name)}(handlers, in_)`;
|
|
1903
|
+
const driven = asyncPlans.get(c.name).runnerIsAsync ? `bc_block_on(${runnerCall})` : runnerCall;
|
|
1904
|
+
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
1905
|
})
|
|
1495
1906
|
.join("\n");
|
|
1496
1907
|
// the scripted handler is passed as `&ScriptedNativeRaw`; the observe entry takes `&ScriptedNativeRaw`
|
|
@@ -1562,6 +1973,30 @@ use super::*;
|
|
|
1562
1973
|
fn to_rt_err(e: BehaviorError) -> behavior_contracts::BehaviorError {
|
|
1563
1974
|
behavior_contracts::BehaviorError::new(e.code, e.message)
|
|
1564
1975
|
}
|
|
1976
|
+
${anyAsyncRunner
|
|
1977
|
+
? `// bc#97 (TEST glue) — a minimal std-only block-on that drives an async runner Future to completion so
|
|
1978
|
+
// observe_native_raw stays sync-callable (the harness never changes). The covered async fn's futures are
|
|
1979
|
+
// ready (the scripted handler does no real I/O), so a no-op waker + spin poll resolves in one step. This
|
|
1980
|
+
// lives ONLY in the observe companion — the covered module itself imports zero runtime.
|
|
1981
|
+
fn bc_block_on<F: std::future::Future>(mut fut: F) -> F::Output {
|
|
1982
|
+
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
|
1983
|
+
fn noop(_: *const ()) {}
|
|
1984
|
+
fn clone(_: *const ()) -> RawWaker {
|
|
1985
|
+
RawWaker::new(std::ptr::null(), &VTABLE)
|
|
1986
|
+
}
|
|
1987
|
+
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);
|
|
1988
|
+
let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
|
|
1989
|
+
let mut cx = Context::from_waker(&waker);
|
|
1990
|
+
// SAFETY: fut is owned on the stack and never moved after pinning (it is dropped at scope end).
|
|
1991
|
+
let mut fut = unsafe { std::pin::Pin::new_unchecked(&mut fut) };
|
|
1992
|
+
loop {
|
|
1993
|
+
match fut.as_mut().poll(&mut cx) {
|
|
1994
|
+
Poll::Ready(v) => return v,
|
|
1995
|
+
Poll::Pending => std::thread::yield_now(),
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}`
|
|
1999
|
+
: ""}
|
|
1565
2000
|
|
|
1566
2001
|
${serializers}
|
|
1567
2002
|
|
|
@@ -1601,7 +2036,6 @@ ${arms}
|
|
|
1601
2036
|
* typed read; Value-typed → the raw Value clone). A missing key leaves Default (every covered input is
|
|
1602
2037
|
* required + supplied by the corpus). Lives ONLY in the observe companion. */
|
|
1603
2038
|
function emitInDecoder(comp, plan) {
|
|
1604
|
-
void plan;
|
|
1605
2039
|
const name = inStructName(comp.name);
|
|
1606
2040
|
const ports = Object.keys(comp.inputPorts ?? {});
|
|
1607
2041
|
const lines = [];
|
|
@@ -1617,10 +2051,26 @@ function emitInDecoder(comp, plan) {
|
|
|
1617
2051
|
lines.push(` for (k, v) in input {`);
|
|
1618
2052
|
ports.forEach((p, i) => {
|
|
1619
2053
|
const field = rustFieldName(p);
|
|
1620
|
-
const
|
|
2054
|
+
const schema = comp.inputPorts[p];
|
|
2055
|
+
const scalar = inputScalarKind(schema);
|
|
2056
|
+
const et = schema.elemType;
|
|
1621
2057
|
const head = i === 0 ? " if" : " } else if";
|
|
1622
2058
|
lines.push(`${head} k == ${rustStrLit(p)} {`);
|
|
1623
|
-
if (
|
|
2059
|
+
if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
2060
|
+
// #108: decode the boxed Value::Arr into the native Vec<ElemT> (test glue — materialize each elem).
|
|
2061
|
+
const elemRef = deriveTypeRef(et, plan);
|
|
2062
|
+
const elemTy = renderTypeRef(elemRef);
|
|
2063
|
+
const mat = materializeExpr("ev", elemRef, plan);
|
|
2064
|
+
lines.push(` match v {`);
|
|
2065
|
+
lines.push(` Value::Arr(a) => {`);
|
|
2066
|
+
lines.push(` let mut dec: Vec<${elemTy}> = Vec::with_capacity(a.len());`);
|
|
2067
|
+
lines.push(` for ev in a.iter() { dec.push(${mat}); }`);
|
|
2068
|
+
lines.push(` in_.${field} = dec;`);
|
|
2069
|
+
lines.push(` }`);
|
|
2070
|
+
lines.push(` other => return Err(marshal_type_error(${rustStrLit(`decode input ${p}: expected array, got `)}, other.type_name())),`);
|
|
2071
|
+
lines.push(` }`);
|
|
2072
|
+
}
|
|
2073
|
+
else if (scalar === undefined) {
|
|
1624
2074
|
lines.push(` in_.${field} = v.clone();`);
|
|
1625
2075
|
}
|
|
1626
2076
|
else {
|