behavior-contracts 0.4.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/README.md +78 -29
- 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 +35 -0
- package/dist/generator/core.d.ts.map +1 -1
- package/dist/generator/core.js +1 -0
- package/dist/generator/core.js.map +1 -1
- package/dist/generator/{emit-straightline-typed-go.d.ts → emit-shared-go-typed.d.ts} +28 -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} +17 -31
- 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} +27 -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} +7 -28
- 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} +9 -30
- 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 +63 -0
- package/dist/generator/emit-typed-native-go.d.ts.map +1 -0
- package/dist/generator/emit-typed-native-go.js +2260 -0
- package/dist/generator/emit-typed-native-go.js.map +1 -0
- package/dist/generator/emit-typed-native-rust.d.ts +53 -0
- package/dist/generator/emit-typed-native-rust.d.ts.map +1 -0
- package/dist/generator/emit-typed-native-rust.js +2123 -0
- package/dist/generator/emit-typed-native-rust.js.map +1 -0
- package/dist/generator/index.d.ts +4 -12
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +16 -52
- 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/straightline.d.ts +46 -0
- package/dist/generator/straightline.d.ts.map +1 -1
- package/dist/generator/straightline.js +65 -0
- package/dist/generator/straightline.js.map +1 -1
- 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 -345
- 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 -58
- 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-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 -1111
- 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 -1153
- 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
|
@@ -0,0 +1,2123 @@
|
|
|
1
|
+
import { GeneratorFailure } from "./core.js";
|
|
2
|
+
import { rustStraightlineInternals } from "./emit-shared-rust.js";
|
|
3
|
+
import { rustTypedInternals } from "./emit-shared-rust-typed.js";
|
|
4
|
+
import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
|
|
5
|
+
import { concurrencyPlan, execPlan, analyzeSequentialOps, classifyExpr } from "./straightline.js";
|
|
6
|
+
import { NativeExprCompiler } from "./native-expr.js";
|
|
7
|
+
import { buildAsyncPlans } from "./async-plan.js";
|
|
8
|
+
const { sanitize, rustStrLit } = rustStraightlineInternals;
|
|
9
|
+
const { rustFieldName, renderTypeRef, typedCell, typedFieldAccess, serializeTyped, emitTypeDecls, emitDefaults, emitSerializers, emitTypedValue, emitMarshallers, emitMarshalHelpers, materializeExpr, } = rustTypedInternals;
|
|
10
|
+
/** rustErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
|
|
11
|
+
* plane (runtime-free). The covered module declares a local `BehaviorError` (see emitBehaviorErrorType);
|
|
12
|
+
* the runner returns `Result<_, BehaviorError>` over THAT local type. This REPLACES the old
|
|
13
|
+
* `behavior_contracts::BehaviorError::new(...)` (the last error-plane runtime coupling) — the SAME codes
|
|
14
|
+
* are preserved verbatim so the observed failure is byte-equal to run_behavior. */
|
|
15
|
+
function rustErr(code, msgExpr) {
|
|
16
|
+
return `BehaviorError::new(${JSON.stringify(code)}, ${msgExpr})`;
|
|
17
|
+
}
|
|
18
|
+
/** emitBehaviorErrorType — the LOCAL concrete error type baked into the covered module so a runner can
|
|
19
|
+
* fail closed with ZERO bc-runtime import. Codes match run_behavior verbatim (byte-equal). `code()` /
|
|
20
|
+
* `failure_code()` expose the stable code without a bc-runtime type. */
|
|
21
|
+
function emitBehaviorErrorType() {
|
|
22
|
+
return `// BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
|
|
23
|
+
// covered runner returns \`Result<T, BehaviorError>\` over THIS local type instead of a bc-runtime
|
|
24
|
+
// failure, so the fully-covered module imports ZERO bc runtime. Codes match run_behavior verbatim
|
|
25
|
+
// (byte-equal). The observe companion (test glue, a super:: submodule) bridges this local error to the
|
|
26
|
+
// bc-runtime BehaviorError at the observe boundary — the covered module itself stays runtime-free.
|
|
27
|
+
#[derive(Debug, Clone)]
|
|
28
|
+
pub struct BehaviorError {
|
|
29
|
+
pub code: String,
|
|
30
|
+
pub message: String,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl BehaviorError {
|
|
34
|
+
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
|
|
35
|
+
BehaviorError { code: code.into(), message: message.into() }
|
|
36
|
+
}
|
|
37
|
+
/// The stable failure code (byte-equal to run_behavior) WITHOUT a bc-runtime type.
|
|
38
|
+
pub fn code(&self) -> &str {
|
|
39
|
+
&self.code
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
impl std::fmt::Display for BehaviorError {
|
|
44
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
45
|
+
write!(f, "{}: {}", self.code, self.message)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
impl std::error::Error for BehaviorError {}`;
|
|
49
|
+
}
|
|
50
|
+
function portFieldName(wire) {
|
|
51
|
+
const safe = wire.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase();
|
|
52
|
+
return `f_${safe}`;
|
|
53
|
+
}
|
|
54
|
+
function pascal(s) {
|
|
55
|
+
return s.replace(/[^A-Za-z0-9]+/g, " ").split(" ").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") || "X";
|
|
56
|
+
}
|
|
57
|
+
function portsStructName(compName, nodeId) {
|
|
58
|
+
return `PortsNR${pascal(compName)}${pascal(nodeId)}`;
|
|
59
|
+
}
|
|
60
|
+
/** RawRowNR<Comp><Node> — the concrete per-node handler-result row struct name. */
|
|
61
|
+
function rawRowStructName(compName, nodeId) {
|
|
62
|
+
return `RawRowNR${pascal(compName)}${pascal(nodeId)}`;
|
|
63
|
+
}
|
|
64
|
+
/** RawElemNR<Comp><Node> — the concrete per-ELEMENT row struct name for a covered map node. */
|
|
65
|
+
function rawElemStructName(compName, nodeId) {
|
|
66
|
+
return `RawElemNR${pascal(compName)}${pascal(nodeId)}`;
|
|
67
|
+
}
|
|
68
|
+
/** HandlerNR<Comp> — the per-component concrete handler trait name (one node_* method per node). */
|
|
69
|
+
function handlerTraitName(compName) {
|
|
70
|
+
return `HandlerNR${pascal(compName)}`;
|
|
71
|
+
}
|
|
72
|
+
/** node_<id> — the concrete per-node handler method name. */
|
|
73
|
+
function nodeMethodName(nodeId) {
|
|
74
|
+
return `node_${nodeId.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase()}`;
|
|
75
|
+
}
|
|
76
|
+
/** InNR<Comp> — the concrete per-component input struct name (fields = inputPorts). */
|
|
77
|
+
function inStructName(compName) {
|
|
78
|
+
return `InNR${pascal(compName)}`;
|
|
79
|
+
}
|
|
80
|
+
function staticArrElems(node) {
|
|
81
|
+
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
82
|
+
return null;
|
|
83
|
+
const keys = Object.keys(node);
|
|
84
|
+
if (keys.length !== 1 || keys[0] !== "arr")
|
|
85
|
+
return null;
|
|
86
|
+
const arg = node.arr;
|
|
87
|
+
return Array.isArray(arg) ? arg : null;
|
|
88
|
+
}
|
|
89
|
+
function inferPortType(node, inputPorts, asBinding) {
|
|
90
|
+
if (staticArrElems(node) !== null)
|
|
91
|
+
return "Value";
|
|
92
|
+
const e = classifyExpr(node);
|
|
93
|
+
switch (e.kind) {
|
|
94
|
+
case "str":
|
|
95
|
+
case "concat":
|
|
96
|
+
return "String";
|
|
97
|
+
case "bool":
|
|
98
|
+
return "bool";
|
|
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
|
+
}
|
|
112
|
+
if (e.path.length === 1) {
|
|
113
|
+
const s = inputPorts[e.path[0]];
|
|
114
|
+
if (s) {
|
|
115
|
+
if (s.type === "string")
|
|
116
|
+
return "String";
|
|
117
|
+
if (s.type === "int")
|
|
118
|
+
return "i64";
|
|
119
|
+
if (s.type === "float")
|
|
120
|
+
return "f64";
|
|
121
|
+
if (s.type === "bool")
|
|
122
|
+
return "bool";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return "Value";
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
return "Value";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** A static string-array port (every element a static string literal) — the graphddb `projection`
|
|
132
|
+
* shape. Returns the list of literal strings, or null when the node is not that shape. */
|
|
133
|
+
function staticStringArrElems(node) {
|
|
134
|
+
const arr = staticArrElems(node);
|
|
135
|
+
if (arr === null)
|
|
136
|
+
return null;
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const el of arr) {
|
|
139
|
+
const e = classifyExpr(el);
|
|
140
|
+
if (e.kind !== "str")
|
|
141
|
+
return null;
|
|
142
|
+
out.push(e.value);
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* portFieldRustType — the NATIVE Rust type for a covered port field (bc#90 / runtime-free). The covered
|
|
148
|
+
* ports struct carries NO `Value` field: every port lowers to a concrete Rust type.
|
|
149
|
+
* - a static string-array (projection) → `Vec<&'static str>` (change #2);
|
|
150
|
+
* - a bare int/float number literal (e.g. Query `limit: 20`) → `i64` / `f64`;
|
|
151
|
+
* - a string/bool/scalar-input port → the scalar Rust type (inferPortType).
|
|
152
|
+
* A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
|
|
153
|
+
* Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no `Value`
|
|
154
|
+
* fallback on the native module). `where` names the emission site for the error.
|
|
155
|
+
*/
|
|
156
|
+
function portFieldRustType(node, inputPorts, where = "port", asBinding) {
|
|
157
|
+
if (staticStringArrElems(node) !== null)
|
|
158
|
+
return "Vec<&'static str>";
|
|
159
|
+
const num = numLiteralRustExpr(node);
|
|
160
|
+
if (num !== null)
|
|
161
|
+
return typeof node === "number" && Number.isInteger(node) ? "i64" : "f64";
|
|
162
|
+
const ft = inferPortType(node, inputPorts, asBinding);
|
|
163
|
+
if (ft === "Value") {
|
|
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.`);
|
|
165
|
+
}
|
|
166
|
+
return ft;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* emitPortsStruct — the CONCRETE native ports struct for a componentRef node. Typed fields per the
|
|
170
|
+
* static port type, constructed directly. NO `impl PortReaderT` / by-name `port()` — the covered runner
|
|
171
|
+
* passes `&PortsNR…` to the concrete `node_*` method by direct generic instantiation, and the handler
|
|
172
|
+
* reads `ports.f_<field>` DIRECTLY. The struct derives Clone (a bc#87 parallel stage clones it into the
|
|
173
|
+
* per-worker dispatch). Every field is a NATIVE Rust type (bc#90 / runtime-free) — NO boxed `Value`.
|
|
174
|
+
*/
|
|
175
|
+
function emitPortsStruct(comp, node, asBinding) {
|
|
176
|
+
const name = portsStructName(comp.name, node.id);
|
|
177
|
+
const portNames = Object.keys(node.ports);
|
|
178
|
+
if (portNames.length === 0) {
|
|
179
|
+
return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
|
|
180
|
+
}
|
|
181
|
+
const types = portNames.map((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding));
|
|
182
|
+
const fields = portNames
|
|
183
|
+
.map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
|
|
184
|
+
.join("\n");
|
|
185
|
+
return `// ${name} — CONCRETE native ports for node '${node.id}' (${node.component}). Typed fields per the
|
|
186
|
+
// static port type; constructed directly (no Vec, no heap key strings, no per-port Value boxing). The
|
|
187
|
+
// handler reads the typed fields directly off this concrete struct — no by-name accessor.
|
|
188
|
+
#[derive(Clone)]
|
|
189
|
+
pub struct ${name} {
|
|
190
|
+
${fields}
|
|
191
|
+
}`;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* emitMapPortsStructs — element ports struct + batch struct for a covered map node (CONCRETE). The batch
|
|
195
|
+
* struct carries the Vec of per-element CONCRETE ports structs (the handler reads them off the concrete
|
|
196
|
+
* struct — no by-name accessor, no key-value object).
|
|
197
|
+
*/
|
|
198
|
+
function emitMapPortsStructs(comp, node, typedNodes, plan) {
|
|
199
|
+
const m = node.map;
|
|
200
|
+
const name = portsStructName(comp.name, node.id);
|
|
201
|
+
const batch = `${name}Batch`;
|
|
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);
|
|
206
|
+
return `${elemStruct}
|
|
207
|
+
|
|
208
|
+
// ${batch} — CONCRETE batched ports for map '${node.id}': the Vec of per-element CONCRETE ports structs
|
|
209
|
+
// (a Vec<${name}>, NOT a generic key-value container). The consumer's batched handler reads the typed
|
|
210
|
+
// element ports off the concrete struct directly.
|
|
211
|
+
#[derive(Clone)]
|
|
212
|
+
pub struct ${batch} {
|
|
213
|
+
pub items: Vec<${name}>,
|
|
214
|
+
}`;
|
|
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
|
+
}
|
|
398
|
+
// ── CONCRETE per-node row structs + row→outType copiers (mirror the Go concrete contract) ──────────
|
|
399
|
+
//
|
|
400
|
+
// Each covered node has a CONCRETE row struct `RawRowNR<Comp><Node>` whose fields are the node's projected
|
|
401
|
+
// outType fields (typed, flattened) plus a per-node error signal (is_error/err). A named-struct outType
|
|
402
|
+
// flattens to one Rust field per outType field; a scalar/arr/opt node carries a single `val: <typed>`
|
|
403
|
+
// payload. The per-component `HandlerNR<Comp>` trait has one method per node returning that concrete
|
|
404
|
+
// struct; the runner reads `row.<field>` DIRECTLY (no `RawValue`, no `match`, no `Value::`) and copies
|
|
405
|
+
// each field into the node's outType struct local.
|
|
406
|
+
/** emitRawRowStructs — emit the concrete `RawRowNR<Comp><Node>` struct per covered node (+ per-element
|
|
407
|
+
* `RawElemNR<Comp><Node>` for a covered map). Every row carries the per-node error signal (is_error/err). */
|
|
408
|
+
function emitRawRowStructs(comp, typedNodes, plan) {
|
|
409
|
+
const parts = [];
|
|
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).
|
|
413
|
+
const ref = typedNodes.get(n.id);
|
|
414
|
+
if ("map" in n) {
|
|
415
|
+
const arrRef = ref;
|
|
416
|
+
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
417
|
+
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
418
|
+
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
|
|
419
|
+
`#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub is_error: bool,\n pub err: String,\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
|
|
423
|
+
}
|
|
424
|
+
return parts.join("\n\n");
|
|
425
|
+
}
|
|
426
|
+
/** Emit one concrete row struct: flatten a named outType to typed fields, else a single `val` payload;
|
|
427
|
+
* always carry is_error/err. Derives Default so a handler can build an error row cheaply (`..Default`). */
|
|
428
|
+
function emitOneRowStruct(name, ref, plan) {
|
|
429
|
+
const lines = [];
|
|
430
|
+
lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType; + error signal).`);
|
|
431
|
+
lines.push(`#[derive(Default)]`);
|
|
432
|
+
lines.push(`pub struct ${name} {`);
|
|
433
|
+
lines.push(` pub is_error: bool,`);
|
|
434
|
+
lines.push(` pub err: String,`);
|
|
435
|
+
if (ref.kind === "named") {
|
|
436
|
+
const decl = findDecl(plan, ref.name);
|
|
437
|
+
for (const f of decl.fields) {
|
|
438
|
+
lines.push(` pub ${rustFieldName(f.name)}: ${renderTypeRef(f.type)}, // ${JSON.stringify(f.name)}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
lines.push(` pub val: ${renderTypeRef(ref)},`);
|
|
443
|
+
}
|
|
444
|
+
lines.push(`}`);
|
|
445
|
+
return lines.join("\n");
|
|
446
|
+
}
|
|
447
|
+
/** emitRowCopy — the statement copying a concrete row's typed fields into the node's outType local
|
|
448
|
+
* `target` (a value of Rust type renderTypeRef(ref)). Named: struct literal from the row's moved fields;
|
|
449
|
+
* scalar/arr/opt: `.val`. `rowExpr` is the concrete row VALUE (moved). No dynamic read — pure move/copy. */
|
|
450
|
+
function emitRowMove(target, rowExpr, ref, plan) {
|
|
451
|
+
if (ref.kind === "named") {
|
|
452
|
+
const decl = findDecl(plan, ref.name);
|
|
453
|
+
const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: ${rowExpr}.${rustFieldName(f.name)}`).join(", ");
|
|
454
|
+
return `${target} = ${ref.name} { ${inits} };`;
|
|
455
|
+
}
|
|
456
|
+
return `${target} = ${rowExpr}.val;`;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* boundRustType (change #3 — typed `bound`) — the CONCRETE Rust type of a covered node's `bound` handler
|
|
460
|
+
* param (bc#90 / runtime-free). `bound` is the parent-produced key (the bindField value); it replaces the
|
|
461
|
+
* old boxed `Value`. We represent PRODUCED-vs-UNPRODUCED with an OWNED `Option<K>`: `Some(v)` = the parent
|
|
462
|
+
* value was produced/present (an empty string is a VALID produced value — `Some("")`); `None` =
|
|
463
|
+
* UNPRODUCED/skip (exactly the old `!= Null` guard ≡ run_behavior). `K` is the bindField's concrete Rust
|
|
464
|
+
* type (a string key here). A node WITHOUT a bindField never receives a bound value — its param is an
|
|
465
|
+
* `Option<String>` placeholder always passed `None` (a typed None, so the ABI stays uniform and carries
|
|
466
|
+
* NO Value).
|
|
467
|
+
*/
|
|
468
|
+
function boundRustType(node, typedNodes, plan) {
|
|
469
|
+
if (node.bindField === undefined || node.parent === undefined)
|
|
470
|
+
return "Option<String>";
|
|
471
|
+
const parentRef = typedNodes.get(node.parent);
|
|
472
|
+
if (parentRef === undefined)
|
|
473
|
+
return "Option<String>";
|
|
474
|
+
let bfRef;
|
|
475
|
+
try {
|
|
476
|
+
bfRef = typedFieldAccess(`${typedCell(node.parent)}.borrow()`, parentRef, [node.bindField], plan).ref;
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return "Option<String>";
|
|
480
|
+
}
|
|
481
|
+
// opt bindField: the field is already `Option<T>` — the bound is that same Option. A required scalar
|
|
482
|
+
// bindField: the bound is `Option<T>` (always Some — the "produced" pointer is never None).
|
|
483
|
+
if (bfRef.kind === "opt")
|
|
484
|
+
return `Option<${renderTypeRef(bfRef.inner)}>`;
|
|
485
|
+
return `Option<${renderTypeRef(bfRef)}>`;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* emitHandlerTrait — the per-component `HandlerNR<Comp>` trait: one concrete-typed `node_*` method per
|
|
489
|
+
* covered node. A componentRef node method takes the node's native ports struct + the typed produced-aware
|
|
490
|
+
* `bound` (change #3 — NO boxed Value) and returns Option<RawRowNR…> (None = unknown component,
|
|
491
|
+
* fail-closed). A covered map node's method takes the batch ports struct (batched) or the element ports
|
|
492
|
+
* struct (per-element). This is the fully-concrete, runtime-free replacement for the NativeRawComponentExec
|
|
493
|
+
* seam.
|
|
494
|
+
*/
|
|
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");
|
|
503
|
+
const lines = [];
|
|
504
|
+
lines.push(`// ${handlerTraitName(comp.name)} — the CONCRETE per-component handler seam: one typed method`);
|
|
505
|
+
lines.push(`// per covered node (native ports struct IN, concrete row struct OUT). No generic boxed-ports`);
|
|
506
|
+
lines.push(`// / boxed-value / dynamic accessor crosses the covered boundary — the consumer implements each`);
|
|
507
|
+
lines.push(`// node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
|
|
508
|
+
lines.push(`pub trait ${handlerTraitName(comp.name)} {`);
|
|
509
|
+
for (const n of comp.body) {
|
|
510
|
+
if ("cond" in n)
|
|
511
|
+
continue; // #108: a cond node has no handler method (pure Expression).
|
|
512
|
+
const method = nodeMethodName(n.id);
|
|
513
|
+
const fnKw = fnKwFor(n.id);
|
|
514
|
+
const bt = boundRustType(n, typedNodes, plan);
|
|
515
|
+
if ("map" in n) {
|
|
516
|
+
const m = n.map;
|
|
517
|
+
const batched = m.batched === true;
|
|
518
|
+
const portsT = batched ? `${portsStructName(comp.name, n.id)}Batch` : portsStructName(comp.name, n.id);
|
|
519
|
+
const retT = batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
|
|
520
|
+
lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Option<${retT}>;`);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
lines.push(` ${fnKw} ${method}(&self, ports: &${portsStructName(comp.name, n.id)}, bound: ${bt}) -> Option<${rawRowStructName(comp.name, n.id)}>;`);
|
|
524
|
+
}
|
|
525
|
+
lines.push(`}`);
|
|
526
|
+
return lines.join("\n");
|
|
527
|
+
}
|
|
528
|
+
/** mapElemMaterializerName — the augmented-element copier for a covered map...into node. */
|
|
529
|
+
function mapElemMaterializerName(nodeId) {
|
|
530
|
+
return `materialize_map_elem_${typedCell(nodeId)}`;
|
|
531
|
+
}
|
|
532
|
+
/** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
|
|
533
|
+
* type (into) or the element outType directly (no-into). This is the type of RawElemNR<Comp><Node>. */
|
|
534
|
+
function mapElemRowRef(node, arrRef, plan) {
|
|
535
|
+
const m = node.map;
|
|
536
|
+
if (m.into === undefined)
|
|
537
|
+
return arrRef.elem;
|
|
538
|
+
if (arrRef.elem.kind !== "named") {
|
|
539
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined map (#86 pt2): node '${node.id}' element outType must be a named struct — regenerate as boxed typed/straight-line.`);
|
|
540
|
+
}
|
|
541
|
+
const decl = findDecl(plan, arrRef.elem.name);
|
|
542
|
+
const intoField = decl.fields.find((f) => f.name === m.into);
|
|
543
|
+
if (!intoField) {
|
|
544
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined map (#86 pt2): element struct '${arrRef.elem.name}' has no '${m.into}' (into) field.`);
|
|
545
|
+
}
|
|
546
|
+
return intoField.type;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* emitMapElemMaterializers — for each covered map...into node, emit the AUGMENTED-element copier: copy
|
|
550
|
+
* the over element's typed fields + move the `into` field from the concrete per-element handler row
|
|
551
|
+
* (RawElemNR). Pure struct field assignment — NO dynamic value read (the handler already produced the
|
|
552
|
+
* typed into row).
|
|
553
|
+
*/
|
|
554
|
+
function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
555
|
+
const parts = [];
|
|
556
|
+
for (const n of comp.body) {
|
|
557
|
+
if (!("map" in n))
|
|
558
|
+
continue;
|
|
559
|
+
const m = n.map;
|
|
560
|
+
// #93 shape 3: a NO-into map materializes each element directly from the per-element row.
|
|
561
|
+
if (m.into === undefined)
|
|
562
|
+
continue;
|
|
563
|
+
const arrRef = typedNodes.get(n.id);
|
|
564
|
+
if (arrRef.kind !== "arr" || arrRef.elem.kind !== "named") {
|
|
565
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined map (#86 pt2): node '${n.id}' element outType must be a named struct — regenerate as boxed typed/straight-line.`);
|
|
566
|
+
}
|
|
567
|
+
const elemRef = arrRef.elem;
|
|
568
|
+
const decl = findDecl(plan, elemRef.name);
|
|
569
|
+
const intoKey = m.into;
|
|
570
|
+
const intoField = decl.fields.find((f) => f.name === intoKey);
|
|
571
|
+
const overFields = decl.fields.filter((f) => f.name !== intoKey);
|
|
572
|
+
const overElemTy = mapOverElemType(comp, n, typedNodes, plan);
|
|
573
|
+
const fn = mapElemMaterializerName(n.id);
|
|
574
|
+
const elemRowGo = rawElemStructName(comp.name, n.id);
|
|
575
|
+
const intoRowRef = mapElemRowRef(n, arrRef, plan);
|
|
576
|
+
const inits = [];
|
|
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
|
+
}
|
|
582
|
+
// move the into field off the concrete element row (named → struct literal; scalar/arr/opt → .val).
|
|
583
|
+
const intoValue = intoRowRef.kind === "named"
|
|
584
|
+
? `${intoRowRef.name} { ${findDecl(plan, intoRowRef.name).fields.map((f) => `${rustFieldName(f.name)}: into.${rustFieldName(f.name)}`).join(", ")} }`
|
|
585
|
+
: `into.val`;
|
|
586
|
+
parts.push(`#[allow(dead_code)]
|
|
587
|
+
fn ${fn}(over: &${overElemTy}, into: ${elemRowGo}) -> ${elemRef.name} {
|
|
588
|
+
${elemRef.name} {
|
|
589
|
+
${inits.join("\n")}
|
|
590
|
+
${rustFieldName(intoKey)}: ${intoValue},
|
|
591
|
+
}
|
|
592
|
+
}`);
|
|
593
|
+
void intoField;
|
|
594
|
+
}
|
|
595
|
+
return parts.join("\n\n");
|
|
596
|
+
}
|
|
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) {
|
|
603
|
+
const m = node.map;
|
|
604
|
+
const e = classifyExpr(m.over);
|
|
605
|
+
if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
|
|
606
|
+
const baseRef = typedNodes.get(e.path[0]);
|
|
607
|
+
const { ref } = typedFieldAccess(`${typedCell(e.path[0])}.borrow()`, baseRef, e.path.slice(1), plan);
|
|
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()` };
|
|
618
|
+
}
|
|
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);
|
|
624
|
+
}
|
|
625
|
+
// ── eligibility ─────────────────────────────────────────────────────────────────────
|
|
626
|
+
function reconstructR(e) {
|
|
627
|
+
switch (e.kind) {
|
|
628
|
+
case "str":
|
|
629
|
+
return e.value;
|
|
630
|
+
case "bool":
|
|
631
|
+
return e.value;
|
|
632
|
+
case "null":
|
|
633
|
+
return null;
|
|
634
|
+
case "ref":
|
|
635
|
+
return { [e.opt ? "refOpt" : "ref"]: e.path };
|
|
636
|
+
case "concat":
|
|
637
|
+
return { concat: e.parts.map(reconstructR) };
|
|
638
|
+
case "dynamic":
|
|
639
|
+
return e.node;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* numLiteralRustExpr — a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
|
|
644
|
+
* `classifyExpr` leaves a bare number `dynamic` (its checked int/float range rules are the evaluate
|
|
645
|
+
* SSoT), so it never reaches the static str/bool/null branch — but a number literal IS statically
|
|
646
|
+
* resolvable to a native `Value`: the SAME range check the runtime applies (integral literal in the
|
|
647
|
+
* safe 2^53-1 window → Value::Int; a fractional/exponent literal → the checked Value::Float) is
|
|
648
|
+
* performed HERE at generation time. Returns the Rust expression for the literal's Value, or null
|
|
649
|
+
* when the node is not a bare number OR an integral literal is out of the safe window (keeping the
|
|
650
|
+
* runtime INVALID_LITERAL semantics — the port is not covered natively, the component falls through).
|
|
651
|
+
* Mirrors behavior_contracts eval of a bare number literal exactly.
|
|
652
|
+
*/
|
|
653
|
+
const NUM_SAFE_INT_R = 9007199254740991; // 2^53 - 1 (expr safeInt)
|
|
654
|
+
function numLiteralRustExpr(node) {
|
|
655
|
+
if (typeof node !== "number" || !Number.isFinite(node))
|
|
656
|
+
return null;
|
|
657
|
+
if (Number.isInteger(node)) {
|
|
658
|
+
if (node < -NUM_SAFE_INT_R || node > NUM_SAFE_INT_R)
|
|
659
|
+
return null; // out of safe window → not covered
|
|
660
|
+
return `Value::Int(${node}i64)`;
|
|
661
|
+
}
|
|
662
|
+
// fractional / exponent literal → checked f64 (finite, guaranteed above).
|
|
663
|
+
return `Value::Float(${node}f64)`;
|
|
664
|
+
}
|
|
665
|
+
/** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
|
|
666
|
+
* resolvable to a native Value with no Obj / no evaluate interpreter. */
|
|
667
|
+
function portIsStatic(node) {
|
|
668
|
+
const arr = staticArrElems(node);
|
|
669
|
+
if (arr !== null)
|
|
670
|
+
return arr.every(portIsStatic);
|
|
671
|
+
if (numLiteralRustExpr(node) !== null)
|
|
672
|
+
return true; // bare number literal (e.g. Query `limit`)
|
|
673
|
+
const e = classifyExpr(node);
|
|
674
|
+
switch (e.kind) {
|
|
675
|
+
case "str":
|
|
676
|
+
case "bool":
|
|
677
|
+
case "null":
|
|
678
|
+
case "ref":
|
|
679
|
+
return true;
|
|
680
|
+
case "concat":
|
|
681
|
+
return e.parts.every((p) => portIsStatic(reconstructR(p)));
|
|
682
|
+
default:
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/** The output must lower to a typed struct/value with no scope read: a ref to a BODY NODE, an
|
|
687
|
+
* obj/arr of such, or a concat. A ref to an input param / non-concat operator is not lowerable. */
|
|
688
|
+
function outputIsNativeLowerable(comp) {
|
|
689
|
+
const bodyIds = new Set(comp.body.map((n) => n.id));
|
|
690
|
+
const walk = (node) => {
|
|
691
|
+
const arr = staticArrElems(node);
|
|
692
|
+
if (arr !== null)
|
|
693
|
+
return arr.every(walk);
|
|
694
|
+
if (node !== null && typeof node === "object" && !Array.isArray(node)) {
|
|
695
|
+
const keys = Object.keys(node);
|
|
696
|
+
if (keys.length === 1 && keys[0] === "obj") {
|
|
697
|
+
const obj = node.obj;
|
|
698
|
+
return Object.keys(obj).every((k) => walk(obj[k]));
|
|
699
|
+
}
|
|
700
|
+
if (keys.length === 1 && keys[0] === "arr")
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
const e = classifyExpr(node);
|
|
704
|
+
if (e.kind === "str" || e.kind === "bool" || e.kind === "null")
|
|
705
|
+
return true;
|
|
706
|
+
if (e.kind === "ref")
|
|
707
|
+
return bodyIds.has(e.path[0]);
|
|
708
|
+
if (e.kind === "concat")
|
|
709
|
+
return e.parts.every((p) => walk(reconstructR(p)));
|
|
710
|
+
return false;
|
|
711
|
+
};
|
|
712
|
+
return walk(comp.output);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet), rust twin of the go predicate.
|
|
716
|
+
*/
|
|
717
|
+
function coveredMapNode(n, bodyIds, comp) {
|
|
718
|
+
if (n === null || typeof n !== "object" || !("map" in n))
|
|
719
|
+
return false;
|
|
720
|
+
const m = n.map;
|
|
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)
|
|
725
|
+
return false;
|
|
726
|
+
if (m.policy !== undefined && m.policy !== "fail")
|
|
727
|
+
return false;
|
|
728
|
+
if (m.relationKind !== undefined && m.relationKind !== "single" && m.relationKind !== "connection")
|
|
729
|
+
return false;
|
|
730
|
+
const overE = classifyExpr(m.over);
|
|
731
|
+
if (overE.kind !== "ref")
|
|
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
|
+
}
|
|
741
|
+
const ports = m.ports;
|
|
742
|
+
for (const p of Object.keys(ports))
|
|
743
|
+
if (!portIsStatic(ports[p]))
|
|
744
|
+
return false;
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
/** A covered read SHAPE with static ports + native-lowerable output (+ covered map...into), OR a
|
|
748
|
+
* real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point reads. */
|
|
749
|
+
function isSequentialComponentRefRead(comp) {
|
|
750
|
+
return coverageRejectReason(comp) === null;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89), rust twin of
|
|
754
|
+
* the go predicate. Returns null when the component IS a covered native read; otherwise a SPECIFIC
|
|
755
|
+
* human-readable reason naming the exact disqualifier and the offending node id. The predicate logic is
|
|
756
|
+
* IDENTICAL to isSequentialComponentRefRead (defined as `coverageRejectReason(c) === null`) so the two
|
|
757
|
+
* can never disagree. This is the message the fail-closed dispatch surfaces per non-native component.
|
|
758
|
+
*/
|
|
759
|
+
function coverageRejectReason(comp) {
|
|
760
|
+
const ep = execPlan(comp);
|
|
761
|
+
if (ep === null)
|
|
762
|
+
return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
|
|
763
|
+
for (const [i] of ep.parallelStageOf) {
|
|
764
|
+
const n = comp.body[i];
|
|
765
|
+
if (!("component" in n))
|
|
766
|
+
return `node '${n.id}' is a non-componentRef member of a parallel stage (not native-covered)`;
|
|
767
|
+
if (n.bindField !== undefined)
|
|
768
|
+
return `node '${n.id}' is a bindField relation child inside a parallel stage (not native-covered)`;
|
|
769
|
+
}
|
|
770
|
+
const bodyIds = new Set(comp.body.map((b) => b.id));
|
|
771
|
+
for (const n of comp.body) {
|
|
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
|
+
}
|
|
779
|
+
if ("map" in n) {
|
|
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)`;
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (!("component" in n))
|
|
785
|
+
return `node '${n.id}' is not a componentRef, map, or cond node (not native-covered)`;
|
|
786
|
+
const cr = n;
|
|
787
|
+
if (cr.policy !== undefined && cr.policy !== "fail")
|
|
788
|
+
return `node '${n.id}' componentRef policy '${cr.policy}' (produced-aware output not yet native-covered)`;
|
|
789
|
+
if (cr.bindField !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
790
|
+
return `node '${n.id}' relationKind '${cr.relationKind ?? "point"}' not native-covered (bindField only supports single|connection)`;
|
|
791
|
+
if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
792
|
+
return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
|
|
793
|
+
for (const p of Object.keys(cr.ports))
|
|
794
|
+
if (portIsStatic(cr.ports[p]) === false)
|
|
795
|
+
return `node '${n.id}' port '${p}' is not statically resolvable`;
|
|
796
|
+
}
|
|
797
|
+
if (!outputIsNativeLowerable(comp))
|
|
798
|
+
return "output is not natively lowerable";
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
function isFullyTyped(comp) {
|
|
802
|
+
if (comp.outputType === undefined)
|
|
803
|
+
return false;
|
|
804
|
+
for (const n of comp.body)
|
|
805
|
+
if (n.outType === undefined)
|
|
806
|
+
return false;
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
function isNativeRaw(comp) {
|
|
810
|
+
return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
|
|
811
|
+
}
|
|
812
|
+
function assertTypedCoverage(comp) {
|
|
813
|
+
if (!isSequentialComponentRefRead(comp))
|
|
814
|
+
return;
|
|
815
|
+
if (isFullyTyped(comp))
|
|
816
|
+
return;
|
|
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).`);
|
|
818
|
+
}
|
|
819
|
+
// ── typed input struct (InNR<Comp>) + NATIVE scalar port lowering ────────────────────────────
|
|
820
|
+
/** Rust type for an input port's declared type (scalar → concrete; array+elemType → Vec<ElemT>; else Value). */
|
|
821
|
+
function inputPortRustType(schema, plan) {
|
|
822
|
+
switch (schema?.type) {
|
|
823
|
+
case "string":
|
|
824
|
+
return "String";
|
|
825
|
+
case "int":
|
|
826
|
+
return "i64";
|
|
827
|
+
case "float":
|
|
828
|
+
return "f64";
|
|
829
|
+
case "bool":
|
|
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
|
+
}
|
|
839
|
+
default:
|
|
840
|
+
return "Value";
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/** The scalar kind an input port declares, or undefined if it is not a native scalar. */
|
|
844
|
+
function inputScalarKind(schema) {
|
|
845
|
+
switch (schema?.type) {
|
|
846
|
+
case "string":
|
|
847
|
+
return "String";
|
|
848
|
+
case "int":
|
|
849
|
+
return "i64";
|
|
850
|
+
case "float":
|
|
851
|
+
return "f64";
|
|
852
|
+
case "bool":
|
|
853
|
+
return "bool";
|
|
854
|
+
default:
|
|
855
|
+
return undefined;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
/** emitInStruct — the concrete input struct declaration for a covered component (fields = inputPorts).
|
|
859
|
+
* Empty inputPorts => empty struct. Derives Default so a consumer/test can build it field-by-field. */
|
|
860
|
+
function emitInStruct(comp, plan) {
|
|
861
|
+
const name = inStructName(comp.name);
|
|
862
|
+
const ports = Object.keys(comp.inputPorts ?? {});
|
|
863
|
+
if (ports.length === 0) {
|
|
864
|
+
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\n#[derive(Default)]\npub struct ${name};`;
|
|
865
|
+
}
|
|
866
|
+
const fields = ports
|
|
867
|
+
.map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p], plan)}, // ${JSON.stringify(p)}`)
|
|
868
|
+
.join("\n");
|
|
869
|
+
return `// ${name} — the CONCRETE input for '${comp.name}' (fields = inputPorts; typed, consumer-built —
|
|
870
|
+
// NO generic Value slice, NO per-field boxing crosses the covered read boundary).
|
|
871
|
+
#[derive(Default)]
|
|
872
|
+
pub struct ${name} {
|
|
873
|
+
${fields}
|
|
874
|
+
}`;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* emitNativeScalar — lower a static port expr to a NATIVE Rust expression of the concrete `want` scalar
|
|
878
|
+
* type, or null if it cannot be statically proven that type. A string literal is native for
|
|
879
|
+
* `want==="string"`, a bool literal for `want==="bool"`. A ref resolves via `resolveRef`. A concat lowers
|
|
880
|
+
* ONLY into `string` and only when EVERY part is a native string, emitting a single `format!("{}{}…", …)`.
|
|
881
|
+
*/
|
|
882
|
+
function emitNativeScalar(node, want, resolveRef) {
|
|
883
|
+
if (staticArrElems(node) !== null)
|
|
884
|
+
return null;
|
|
885
|
+
const e = classifyExpr(node);
|
|
886
|
+
switch (e.kind) {
|
|
887
|
+
case "str":
|
|
888
|
+
return want === "String" ? `${rustStrLit(e.value)}.to_string()` : null;
|
|
889
|
+
case "bool":
|
|
890
|
+
return want === "bool" ? (e.value ? "true" : "false") : null;
|
|
891
|
+
case "null":
|
|
892
|
+
return null;
|
|
893
|
+
case "ref": {
|
|
894
|
+
const sc = resolveRef(e.path[0], e.path.slice(1), e.opt);
|
|
895
|
+
if (sc === null || sc.scalar !== want)
|
|
896
|
+
return null;
|
|
897
|
+
return sc.expr;
|
|
898
|
+
}
|
|
899
|
+
case "concat": {
|
|
900
|
+
if (want !== "String")
|
|
901
|
+
return null;
|
|
902
|
+
// a single-part concat is just the owned String of that part.
|
|
903
|
+
if (e.parts.length === 1)
|
|
904
|
+
return emitNativeScalar(reconstructR(e.parts[0]), "String", resolveRef);
|
|
905
|
+
// multi-part: fold into ONE format! (statically strings only — the concat's strings-only
|
|
906
|
+
// TYPE_MISMATCH can never fire, so dropping the check is sound). To stay clippy-clean
|
|
907
|
+
// (to_string_in_format_args), a string-literal part is baked into the format STRING itself
|
|
908
|
+
// (escaped) rather than passed as a `{}` arg; a ref part becomes a `{}` arg (Display on String).
|
|
909
|
+
let fmt = "";
|
|
910
|
+
const args = [];
|
|
911
|
+
for (const p of e.parts) {
|
|
912
|
+
const sub = classifyExpr(reconstructR(p));
|
|
913
|
+
if (sub.kind === "str") {
|
|
914
|
+
fmt += sub.value.replace(/[{}]/g, (c) => c + c); // escape { } for the format string
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
const arg = emitNativeScalar(reconstructR(p), "String", resolveRef);
|
|
918
|
+
if (arg === null)
|
|
919
|
+
return null;
|
|
920
|
+
fmt += "{}";
|
|
921
|
+
args.push(arg);
|
|
922
|
+
}
|
|
923
|
+
if (args.length === 0)
|
|
924
|
+
return `${rustStrLit(fmt)}.to_string()`;
|
|
925
|
+
return `format!(${rustStrLit(fmt)}, ${args.join(", ")})`;
|
|
926
|
+
}
|
|
927
|
+
default:
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
// bc#90 / runtime-free: the old `emitStaticR` / `inputLeafR` / `valueLeafR` port lowering (which built
|
|
932
|
+
// `Value` leaves via the primitives::* helpers and struct-field serializers) is REMOVED — every covered
|
|
933
|
+
// port now lowers to a FULLY NATIVE Rust value in emitPortInit (string / bool / static-string-array /
|
|
934
|
+
// number literal), and a genuinely-dynamic port fails closed. There is no boxed-Value fallback on the
|
|
935
|
+
// covered read plane.
|
|
936
|
+
/**
|
|
937
|
+
* emitPortInit — emit the port field build for one port as a FULLY NATIVE Rust expression (bc#90 /
|
|
938
|
+
* runtime-free). Every covered port lowers to a concrete Rust value assigned straight into the ports
|
|
939
|
+
* struct field — NO `Value`, NO fallback path, NO `match`:
|
|
940
|
+
* - a static string array (projection) → `vec!["a", "b", ...]` (change #2);
|
|
941
|
+
* - a bare int/float number literal (e.g. Query `limit: 20`) → `20i64` / `<n>f64`;
|
|
942
|
+
* - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
|
|
943
|
+
* A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered plane —
|
|
944
|
+
* a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
|
|
945
|
+
* ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
|
|
946
|
+
*/
|
|
947
|
+
function emitPortInit(pn, expr, fieldRustType, resolveRef) {
|
|
948
|
+
const field = portFieldName(pn);
|
|
949
|
+
// change #2: a static string array (projection) → a native `vec![...]` of &'static str.
|
|
950
|
+
const strArr = staticStringArrElems(expr);
|
|
951
|
+
if (strArr !== null) {
|
|
952
|
+
if (fieldRustType !== "Vec<&'static str>") {
|
|
953
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90): port '${pn}' is a static string array but its field type is '${fieldRustType}' (expected Vec<&'static str>).`);
|
|
954
|
+
}
|
|
955
|
+
return `${field}: vec![${strArr.map((s) => rustStrLit(s)).join(", ")}]`;
|
|
956
|
+
}
|
|
957
|
+
// a bare number literal (e.g. Query `limit: 20`): the SSoT range check ran in numLiteralRustExpr —
|
|
958
|
+
// emit the concrete native i64/f64 (unwrap the Value::Int/Float box: it was only there for the old
|
|
959
|
+
// boxed path; the range check is what matters, and it already passed).
|
|
960
|
+
const num = numLiteralRustExpr(expr);
|
|
961
|
+
if (num !== null) {
|
|
962
|
+
if (fieldRustType === "i64")
|
|
963
|
+
return `${field}: ${expr}i64`;
|
|
964
|
+
if (fieldRustType === "f64")
|
|
965
|
+
return `${field}: ${expr}f64`;
|
|
966
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90): port '${pn}' is a number literal but its field type is '${fieldRustType}'.`);
|
|
967
|
+
}
|
|
968
|
+
// string / bool / scalar-input / concat → native scalar expression (no Value, no `match`).
|
|
969
|
+
const native = emitNativeScalar(expr, fieldRustType, resolveRef);
|
|
970
|
+
if (native !== null) {
|
|
971
|
+
return `${field}: ${native}`;
|
|
972
|
+
}
|
|
973
|
+
// FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would re-couple
|
|
974
|
+
// the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the component is not
|
|
975
|
+
// native-eligible and must regenerate on the boxed straight-line path.
|
|
976
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90 / runtime-free): port '${pn}' (${JSON.stringify(expr)}) does not lower to a native Rust value of type '${fieldRustType}'. The covered read plane admits NO boxed Value escape — regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
|
|
977
|
+
}
|
|
978
|
+
// ── the combined runner (STRUCT-returning; INLINE sequential; native ports in; concrete row out) ──
|
|
979
|
+
/** resolveRefAt for the top-level runner: a ref head is a PRIOR NODE (its typed scalar field) or an
|
|
980
|
+
* INPUT PORT (in_.<field>). A REQUIRED scalar resolves to an OWNED native Rust expression; opt / non-
|
|
981
|
+
* scalar / refOpt → null (fall back to the Value path). */
|
|
982
|
+
function runnerResolveRef(priorNodeAt, atPos, typedNodes, plan, inputPorts) {
|
|
983
|
+
return (head, restPath, opt) => {
|
|
984
|
+
if (opt)
|
|
985
|
+
return null;
|
|
986
|
+
if (priorNodeAt(head, atPos)) {
|
|
987
|
+
const baseRef = typedNodes.get(head);
|
|
988
|
+
let acc;
|
|
989
|
+
let expr;
|
|
990
|
+
try {
|
|
991
|
+
const r = typedFieldAccess(`${typedCell(head)}.borrow()`, baseRef, restPath, plan);
|
|
992
|
+
expr = r.expr;
|
|
993
|
+
acc = r.ref;
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
999
|
+
return null;
|
|
1000
|
+
// OWNED: a string field is cloned; int/float/bool are Copy (deref off the Ref).
|
|
1001
|
+
const owned = acc.scalar === "string" ? `(${expr}).clone()` : `*(${expr})`;
|
|
1002
|
+
return { expr: owned, scalar: rustScalar(acc.scalar) };
|
|
1003
|
+
}
|
|
1004
|
+
if (restPath.length !== 0)
|
|
1005
|
+
return null;
|
|
1006
|
+
const scalar = inputScalarKind(inputPorts?.[head]);
|
|
1007
|
+
if (scalar === undefined)
|
|
1008
|
+
return null;
|
|
1009
|
+
const field = `in_.${rustFieldName(head)}`;
|
|
1010
|
+
const owned = scalar === "String" ? `${field}.clone()` : field;
|
|
1011
|
+
return { expr: owned, scalar };
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
function rustScalar(s) {
|
|
1015
|
+
return s === "string" ? "String" : s === "int" ? "i64" : s === "float" ? "f64" : "bool";
|
|
1016
|
+
}
|
|
1017
|
+
function emitNativeRawRunner(comp, plan, ap) {
|
|
1018
|
+
const ep = execPlan(comp);
|
|
1019
|
+
const order = ep.order;
|
|
1020
|
+
const hasConcurrency = ep.parallelStageOf.size > 0;
|
|
1021
|
+
const fn = sanitize(comp.name);
|
|
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.
|
|
1030
|
+
if (isAsync && hasConcurrency) {
|
|
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.`);
|
|
1032
|
+
}
|
|
1033
|
+
const typedNodes = new Map();
|
|
1034
|
+
for (const n of comp.body)
|
|
1035
|
+
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1036
|
+
const idToIndex = new Map();
|
|
1037
|
+
comp.body.forEach((n, i) => idToIndex.set(n.id, i));
|
|
1038
|
+
const opsLiteral = comp.body.map((n) => {
|
|
1039
|
+
const cr = n;
|
|
1040
|
+
const pid = cr.parent;
|
|
1041
|
+
return { id: n.id, parent: pid !== undefined ? idToIndex.get(pid) ?? null : null, policy: cr.policy, bindField: cr.bindField };
|
|
1042
|
+
});
|
|
1043
|
+
const metas = analyzeSequentialOps(opsLiteral, order);
|
|
1044
|
+
const idToPos = new Map();
|
|
1045
|
+
order.forEach((opIdx, k) => idToPos.set(comp.body[opIdx].id, k));
|
|
1046
|
+
const priorNodeAt = (head, atPos) => {
|
|
1047
|
+
const p = idToPos.get(head);
|
|
1048
|
+
return p !== undefined && p < atPos;
|
|
1049
|
+
};
|
|
1050
|
+
const outRef = comp.outputType !== undefined ? deriveTypeRef(comp.outputType, plan) : undefined;
|
|
1051
|
+
const outTy = outRef ? renderTypeRef(outRef) : "Value";
|
|
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.
|
|
1055
|
+
const readsInput = (() => {
|
|
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));
|
|
1071
|
+
};
|
|
1072
|
+
return [...order.keys()].some((k) => {
|
|
1073
|
+
const nd = comp.body[order[k]];
|
|
1074
|
+
if ("map" in nd) {
|
|
1075
|
+
const m = nd.map;
|
|
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());
|
|
1087
|
+
}
|
|
1088
|
+
const cr = nd;
|
|
1089
|
+
return Object.values(cr.ports).some((pn) => walk(pn, k, new Set()));
|
|
1090
|
+
});
|
|
1091
|
+
})();
|
|
1092
|
+
const inParam = readsInput ? "in_" : "_in_";
|
|
1093
|
+
const lines = [];
|
|
1094
|
+
lines.push(`// run_native_raw_struct_${fn} — the STRUCT-RETURNING combined read (bc#77/#87/#94): the fully`);
|
|
1095
|
+
lines.push(`// de-plumbed CONCRETE path. Generic over the per-component CONCRETE ${handlerTraitName(comp.name)} trait,`);
|
|
1096
|
+
lines.push(`// whose node_* methods take the node's native ports struct and return the concrete per-node row`);
|
|
1097
|
+
lines.push(`// struct (typed fields = the outType). The runner builds each ports struct by direct native`);
|
|
1098
|
+
lines.push(`// construction (a simple string port lowers to a native Rust expr), dispatches the concrete`);
|
|
1099
|
+
lines.push(`// node_* method, and reads row.<field> DIRECTLY into the node's outType struct cell — no boxed`);
|
|
1100
|
+
lines.push(`// handler result, no generic enum crossing, no dispatch on a dynamic value on the covered plane. Node`);
|
|
1101
|
+
lines.push(`// results are typed struct cells; a relation child reads the parent's REAL struct result via`);
|
|
1102
|
+
lines.push(`// direct field access (child-present decision from the real parent value — relationSingle /`);
|
|
1103
|
+
lines.push(`// connection converge). A real-concurrency stage (bc#87) is static parallel orchestration —`);
|
|
1104
|
+
lines.push(`// scoped worker threads (bounded by the static plan.concurrency) call the concrete node_*`);
|
|
1105
|
+
lines.push(`// method; preflight + interpret are committed in ascending index order so the value / op`);
|
|
1106
|
+
lines.push(`// multiset / failure precedence byte-match run_behavior. The output is a typed struct/value`);
|
|
1107
|
+
lines.push(`// assembled by struct literal + field access — the consumer keeps it native.`);
|
|
1108
|
+
const bound = hasConcurrency ? `<H: ${handlerTraitName(comp.name)} + Sync>` : `<H: ${handlerTraitName(comp.name)}>`;
|
|
1109
|
+
// bc#97: async propagation (uniform) — if the terminal handlers are async, every handler call awaits,
|
|
1110
|
+
// so the runner is itself `async fn` and returns a future the (already-async) consumer awaits. Sync
|
|
1111
|
+
// default emits `pub fn` exactly as before (byte-identical).
|
|
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);
|
|
1115
|
+
lines.push(`${fnKw} run_native_raw_struct_${fn}${bound}(`);
|
|
1116
|
+
lines.push(` ${dispatchesHandler ? "handlers" : "_handlers"}: &H,`);
|
|
1117
|
+
lines.push(` ${inParam}: ${inStructName(comp.name)},`);
|
|
1118
|
+
lines.push(`) -> Result<${outTy}, BehaviorError> {`);
|
|
1119
|
+
for (const k of order.keys()) {
|
|
1120
|
+
const id = comp.body[order[k]].id;
|
|
1121
|
+
lines.push(` let ${typedCell(id)}: RefCell<${renderTypeRef(typedNodes.get(id))}> = RefCell::new(Default::default());`);
|
|
1122
|
+
lines.push(` let produced_${sanitize(id)} = std::cell::Cell::new(false);`);
|
|
1123
|
+
lines.push(` let _ = &produced_${sanitize(id)};`);
|
|
1124
|
+
}
|
|
1125
|
+
for (let k = 0; k < order.length; k++) {
|
|
1126
|
+
const i = order[k];
|
|
1127
|
+
const stage = ep.parallelStageOf.get(i);
|
|
1128
|
+
if (stage !== undefined && stage[0] === i) {
|
|
1129
|
+
lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt));
|
|
1130
|
+
k += stage.length - 1;
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
if ("map" in comp.body[i]) {
|
|
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));
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
const node = comp.body[i];
|
|
1143
|
+
const meta = metas[i];
|
|
1144
|
+
const s = sanitize(node.id);
|
|
1145
|
+
const structName = portsStructName(comp.name, node.id);
|
|
1146
|
+
const portNames = Object.keys(node.ports);
|
|
1147
|
+
const ref = typedNodes.get(node.id);
|
|
1148
|
+
lines.push(` // ── op '${node.id}' (${node.component}${node.relationKind ? ", relationKind:" + node.relationKind : ""}${node.parent ? ", parent:" + node.parent : ""}) ──`);
|
|
1149
|
+
let indent = " ";
|
|
1150
|
+
const closers = [];
|
|
1151
|
+
if (node.parent !== undefined && priorNodeAt(node.parent, k)) {
|
|
1152
|
+
lines.push(`${indent}if produced_${sanitize(node.parent)}.get() {`);
|
|
1153
|
+
closers.unshift(`${indent}}`);
|
|
1154
|
+
indent += " ";
|
|
1155
|
+
if (node.bindField !== undefined) {
|
|
1156
|
+
// bind (change #3 — typed produced-aware bound): read the parent struct field DIRECTLY (typed
|
|
1157
|
+
// access — no serialize). `bound_<s>` is a typed `Option<K>` — None => UNPRODUCED/skip, Some =>
|
|
1158
|
+
// produced (an empty string is a VALID produced value, `Some("")`). This is the EXACT `!= Null`
|
|
1159
|
+
// skip-gate ≡ run_behavior's preflightOp, now runtime-free (NO Value).
|
|
1160
|
+
const parentRef = typedNodes.get(node.parent);
|
|
1161
|
+
const { expr: bfExpr, ref: bfRef } = typedFieldAccess(`${typedCell(node.parent)}.borrow()`, parentRef, [node.bindField], plan);
|
|
1162
|
+
if (bfRef.kind === "opt") {
|
|
1163
|
+
// an opt bindField is ALREADY an Option<T>: None => skip, Some => produced. Clone it directly.
|
|
1164
|
+
lines.push(`${indent}let bound_${s}: ${boundRustType(node, typedNodes, plan)} = ${bfExpr}.clone();`);
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
// a required scalar bindField is always PRESENT (never skips on null): wrap in Some so the
|
|
1168
|
+
// produced Option is never None, preserving the "produced" semantics of the old guard.
|
|
1169
|
+
const owned = bfRef.kind === "scalar" && bfRef.scalar === "string" ? `(${bfExpr}).clone()` : `*(${bfExpr})`;
|
|
1170
|
+
lines.push(`${indent}let bound_${s}: ${boundRustType(node, typedNodes, plan)} = Some(${owned});`);
|
|
1171
|
+
}
|
|
1172
|
+
lines.push(`${indent}if bound_${s}.is_some() {`);
|
|
1173
|
+
closers.unshift(`${indent}}`);
|
|
1174
|
+
indent += " ";
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
const resolveRef = runnerResolveRef(priorNodeAt, k, typedNodes, plan, comp.inputPorts);
|
|
1178
|
+
const inits = [];
|
|
1179
|
+
for (const pn of portNames) {
|
|
1180
|
+
const expr = node.ports[pn];
|
|
1181
|
+
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`);
|
|
1182
|
+
inits.push(emitPortInit(pn, expr, ty, resolveRef));
|
|
1183
|
+
}
|
|
1184
|
+
const boundArg = node.bindField !== undefined ? `bound_${s}` : "None";
|
|
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" : "";
|
|
1189
|
+
lines.push(`${indent}let ports_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
1190
|
+
lines.push(`${indent}let row_${s} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
|
|
1191
|
+
lines.push(`${indent} Some(r) => r,`);
|
|
1192
|
+
lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(node.component)})),`);
|
|
1193
|
+
lines.push(`${indent}};`);
|
|
1194
|
+
if (meta.policy === "continue") {
|
|
1195
|
+
lines.push(`${indent}if !row_${s}.is_error {`);
|
|
1196
|
+
lines.push(`${indent} ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
1197
|
+
lines.push(`${indent} produced_${s}.set(true);`);
|
|
1198
|
+
lines.push(`${indent}}`);
|
|
1199
|
+
}
|
|
1200
|
+
else {
|
|
1201
|
+
const label = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
|
|
1202
|
+
lines.push(`${indent}if row_${s}.is_error {`);
|
|
1203
|
+
lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under '${label}: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
|
|
1204
|
+
lines.push(`${indent}}`);
|
|
1205
|
+
lines.push(`${indent}${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
1206
|
+
lines.push(`${indent}produced_${s}.set(true);`);
|
|
1207
|
+
}
|
|
1208
|
+
for (const c of closers)
|
|
1209
|
+
lines.push(c);
|
|
1210
|
+
}
|
|
1211
|
+
// output: assemble a TYPED struct/value (struct literal + typed field access — ZERO Value::Obj).
|
|
1212
|
+
const skippable = new Set();
|
|
1213
|
+
const skippableKind = new Map();
|
|
1214
|
+
for (const k of order.keys()) {
|
|
1215
|
+
if (metas[order[k]].canSkip) {
|
|
1216
|
+
const bodyNode = comp.body[order[k]];
|
|
1217
|
+
skippable.add(bodyNode.id);
|
|
1218
|
+
const rk = "map" in bodyNode ? bodyNode.map.relationKind : bodyNode.relationKind;
|
|
1219
|
+
skippableKind.set(bodyNode.id, rk);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
const out = skippable.size > 0
|
|
1223
|
+
? emitProducedAwareValue(comp.output, outRef, typedNodes, plan, skippable, skippableKind)
|
|
1224
|
+
: emitTypedValue(comp.output, outRef, typedNodes, plan);
|
|
1225
|
+
lines.push(` let __out = ${out.expr};`);
|
|
1226
|
+
lines.push(` Ok(__out)`);
|
|
1227
|
+
lines.push(`}`);
|
|
1228
|
+
return lines.join("\n");
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* emitParallelStageArm (bc#87/#94, rust CONCRETE) — EXPLICIT static parallel orchestration for a real-
|
|
1232
|
+
* concurrency stage. Reproduces run_plan_parallel's deterministic protocol so the observed value / op
|
|
1233
|
+
* multiset / failure precedence byte-match run_behavior:
|
|
1234
|
+
* 1. PREFLIGHT (ascending): parent-produced gate + build each member's CONCRETE ports struct.
|
|
1235
|
+
* 2. DISPATCH (bounded scoped threads): share `&H` (the concrete trait; `H: Sync`), pull runnable
|
|
1236
|
+
* members from a shared atomic cursor in ascending order, call the concrete node_* method. Each
|
|
1237
|
+
* concrete row lands in a per-member Mutex slot (None = unknown component).
|
|
1238
|
+
* 3. COMMIT (ascending index): interpret policy + move the concrete row into the outType cell —
|
|
1239
|
+
* lowest-index failure wins.
|
|
1240
|
+
*/
|
|
1241
|
+
function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt) {
|
|
1242
|
+
const lines = [];
|
|
1243
|
+
const ids = stage.map((i) => comp.body[i].id);
|
|
1244
|
+
lines.push(` // ── bc#87 real-concurrency stage {${ids.map((x) => rustStrLit(x)).join(", ")}} — static parallel`);
|
|
1245
|
+
lines.push(` // orchestration (scoped threads, bound=${concurrency}); preflight+commit in ascending index order.`);
|
|
1246
|
+
lines.push(` {`);
|
|
1247
|
+
// 1) PREFLIGHT (ascending): build each member's CONCRETE ports struct into a stage-scoped Option local.
|
|
1248
|
+
for (const i of stage) {
|
|
1249
|
+
const node = comp.body[i];
|
|
1250
|
+
const s = sanitize(node.id);
|
|
1251
|
+
const structName = portsStructName(comp.name, node.id);
|
|
1252
|
+
const gated = node.parent !== undefined && priorNodeAt(node.parent, atPos);
|
|
1253
|
+
let ind = " ";
|
|
1254
|
+
const closers = [];
|
|
1255
|
+
if (gated) {
|
|
1256
|
+
lines.push(` let mut ports_${s}: Option<${structName}> = None;`);
|
|
1257
|
+
lines.push(`${ind}if produced_${sanitize(node.parent)}.get() {`);
|
|
1258
|
+
closers.unshift(`${ind}}`);
|
|
1259
|
+
ind += " ";
|
|
1260
|
+
}
|
|
1261
|
+
const resolveRef = runnerResolveRef(priorNodeAt, atPos, typedNodes, plan, comp.inputPorts);
|
|
1262
|
+
const inits = [];
|
|
1263
|
+
for (const pn of Object.keys(node.ports)) {
|
|
1264
|
+
const expr = node.ports[pn];
|
|
1265
|
+
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`);
|
|
1266
|
+
inits.push(emitPortInit(pn, expr, ty, resolveRef));
|
|
1267
|
+
}
|
|
1268
|
+
if (gated) {
|
|
1269
|
+
lines.push(`${ind}ports_${s} = Some(${structName} { ${inits.join(", ")} });`);
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
lines.push(`${ind}let ports_${s}: Option<${structName}> = Some(${structName} { ${inits.join(", ")} });`);
|
|
1273
|
+
}
|
|
1274
|
+
for (const c of closers)
|
|
1275
|
+
lines.push(c);
|
|
1276
|
+
}
|
|
1277
|
+
// 2) DISPATCH (bounded scoped threads). Share `&H` (concrete trait; H: Sync). Each worker calls the
|
|
1278
|
+
// concrete node_* method for its member's CONCRETE ports and stores the concrete row in its slot.
|
|
1279
|
+
const nMembers = stage.length;
|
|
1280
|
+
const boundN = Math.min(concurrency, nMembers);
|
|
1281
|
+
const jobsVar = `jobs_${sanitize(comp.name)}`;
|
|
1282
|
+
for (const i of stage) {
|
|
1283
|
+
const s = sanitize(comp.body[i].id);
|
|
1284
|
+
const rowT = rawRowStructName(comp.name, comp.body[i].id);
|
|
1285
|
+
lines.push(` let slot_${s}: std::sync::Mutex<Option<Option<${rowT}>>> = std::sync::Mutex::new(None);`);
|
|
1286
|
+
}
|
|
1287
|
+
lines.push(` let mut ${jobsVar}: Vec<usize> = Vec::new();`);
|
|
1288
|
+
stage.forEach((i, slot) => {
|
|
1289
|
+
const s = sanitize(comp.body[i].id);
|
|
1290
|
+
lines.push(` if ports_${s}.is_some() { ${jobsVar}.push(${slot}); }`);
|
|
1291
|
+
});
|
|
1292
|
+
lines.push(` let cursor = std::sync::atomic::AtomicUsize::new(0);`);
|
|
1293
|
+
lines.push(` let workers = ${boundN}usize.min(${jobsVar}.len());`);
|
|
1294
|
+
lines.push(` std::thread::scope(|scope| {`);
|
|
1295
|
+
lines.push(` for _ in 0..workers {`);
|
|
1296
|
+
lines.push(` scope.spawn(|| loop {`);
|
|
1297
|
+
lines.push(` let j = cursor.fetch_add(1, std::sync::atomic::Ordering::SeqCst);`);
|
|
1298
|
+
lines.push(` if j >= ${jobsVar}.len() { break; }`);
|
|
1299
|
+
lines.push(` let slot = ${jobsVar}[j];`);
|
|
1300
|
+
lines.push(` match slot {`);
|
|
1301
|
+
stage.forEach((i, slot) => {
|
|
1302
|
+
const node = comp.body[i];
|
|
1303
|
+
const s = sanitize(node.id);
|
|
1304
|
+
lines.push(` ${slot} => {`);
|
|
1305
|
+
lines.push(` let ports = ports_${s}.as_ref().expect("runnable member has ports");`);
|
|
1306
|
+
lines.push(` let row = handlers.${nodeMethodName(node.id)}(ports, None);`);
|
|
1307
|
+
lines.push(` *slot_${s}.lock().unwrap() = Some(row);`);
|
|
1308
|
+
lines.push(` }`);
|
|
1309
|
+
});
|
|
1310
|
+
lines.push(` _ => unreachable!(),`);
|
|
1311
|
+
lines.push(` };`);
|
|
1312
|
+
lines.push(` });`);
|
|
1313
|
+
lines.push(` }`);
|
|
1314
|
+
lines.push(` });`);
|
|
1315
|
+
// 3) COMMIT (ascending index): interpret + move the concrete row into the outType cell.
|
|
1316
|
+
stage.forEach((i) => {
|
|
1317
|
+
const node = comp.body[i];
|
|
1318
|
+
const meta = metas[i];
|
|
1319
|
+
const s = sanitize(node.id);
|
|
1320
|
+
const ref = typedNodes.get(node.id);
|
|
1321
|
+
lines.push(` if ports_${s}.is_some() {`);
|
|
1322
|
+
lines.push(` let row_${s} = match slot_${s}.lock().unwrap().take() {`);
|
|
1323
|
+
lines.push(` Some(Some(r)) => r,`);
|
|
1324
|
+
lines.push(` _ => return Err(unknown_component(${rustStrLit(node.component)})),`);
|
|
1325
|
+
lines.push(` };`);
|
|
1326
|
+
if (meta.policy === "continue") {
|
|
1327
|
+
lines.push(` if !row_${s}.is_error {`);
|
|
1328
|
+
lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
1329
|
+
lines.push(` produced_${s}.set(true);`);
|
|
1330
|
+
lines.push(` }`);
|
|
1331
|
+
}
|
|
1332
|
+
else {
|
|
1333
|
+
const label = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
|
|
1334
|
+
lines.push(` if row_${s}.is_error {`);
|
|
1335
|
+
lines.push(` return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under '${label}: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
|
|
1336
|
+
lines.push(` }`);
|
|
1337
|
+
lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
1338
|
+
lines.push(` produced_${s}.set(true);`);
|
|
1339
|
+
}
|
|
1340
|
+
lines.push(` }`);
|
|
1341
|
+
});
|
|
1342
|
+
lines.push(` }`);
|
|
1343
|
+
return lines.join("\n");
|
|
1344
|
+
}
|
|
1345
|
+
// bc#90 / runtime-free: the old `emitMapStaticR` map-element port lowering (a Value fallback) is REMOVED
|
|
1346
|
+
// — covered map element ports lower to a fully native Rust value in emitPortInit (the map resolveRef
|
|
1347
|
+
// reads the $as element / prior-node / input typed fields directly), with a fail-closed on a dynamic port.
|
|
1348
|
+
/**
|
|
1349
|
+
* emitMapArm — the struct-native exec of a covered map node (rust CONCRETE). Three shapes:
|
|
1350
|
+
* - #86 pt2: BATCHED map...into — dispatch once (concrete batch row), augmented-element materializer.
|
|
1351
|
+
* - #93 shape 2: NON-batched map...into — dispatch ONCE PER ELEMENT (N physical requests), into per element.
|
|
1352
|
+
* - #93 shape 3: BATCHED map NO into — dispatch once, collect each element from the concrete element row.
|
|
1353
|
+
*/
|
|
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) {
|
|
1358
|
+
const m = node.map;
|
|
1359
|
+
const awaitSfx = isAsync ? ".await" : "";
|
|
1360
|
+
const batched = m.batched === true;
|
|
1361
|
+
const hasInto = m.into !== undefined;
|
|
1362
|
+
const hasGuard = m.when !== undefined;
|
|
1363
|
+
const s = sanitize(node.id);
|
|
1364
|
+
const structName = portsStructName(comp.name, node.id);
|
|
1365
|
+
const batch = `${structName}Batch`;
|
|
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);
|
|
1368
|
+
const elemMat = mapElemMaterializerName(node.id);
|
|
1369
|
+
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1370
|
+
const asName = m.as;
|
|
1371
|
+
const portNames = Object.keys(m.ports);
|
|
1372
|
+
const elemArrRef = typedNodes.get(node.id);
|
|
1373
|
+
const elemName = renderTypeRef(elemArrRef.elem);
|
|
1374
|
+
const resolveRef = (head, restPath, opt) => {
|
|
1375
|
+
if (opt)
|
|
1376
|
+
return null;
|
|
1377
|
+
let baseExpr;
|
|
1378
|
+
let baseRef;
|
|
1379
|
+
if (head === asName) {
|
|
1380
|
+
baseExpr = `oel_${s}`;
|
|
1381
|
+
baseRef = overElemRef;
|
|
1382
|
+
}
|
|
1383
|
+
else if (priorHere(head)) {
|
|
1384
|
+
baseExpr = `${typedCell(head)}.borrow()`;
|
|
1385
|
+
baseRef = typedNodes.get(head);
|
|
1386
|
+
}
|
|
1387
|
+
else {
|
|
1388
|
+
if (restPath.length !== 0)
|
|
1389
|
+
return null;
|
|
1390
|
+
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1391
|
+
if (scalar === undefined)
|
|
1392
|
+
return null;
|
|
1393
|
+
const field = `in_.${rustFieldName(head)}`;
|
|
1394
|
+
return { expr: scalar === "String" ? `${field}.clone()` : field, scalar };
|
|
1395
|
+
}
|
|
1396
|
+
let acc;
|
|
1397
|
+
let expr;
|
|
1398
|
+
try {
|
|
1399
|
+
const r = typedFieldAccess(baseExpr, baseRef, restPath, plan);
|
|
1400
|
+
expr = r.expr;
|
|
1401
|
+
acc = r.ref;
|
|
1402
|
+
}
|
|
1403
|
+
catch {
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1407
|
+
return null;
|
|
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
|
+
}
|
|
1419
|
+
return { expr: owned, scalar: rustScalar(acc.scalar) };
|
|
1420
|
+
};
|
|
1421
|
+
// build the per-element CONCRETE native ports struct. `il` = indent inside the element loop.
|
|
1422
|
+
const buildPorts = (il) => {
|
|
1423
|
+
const out = [];
|
|
1424
|
+
const inits = [];
|
|
1425
|
+
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1426
|
+
for (const pn of portNames) {
|
|
1427
|
+
const ty = portFieldRustType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding);
|
|
1428
|
+
inits.push(emitPortInit(pn, m.ports[pn], ty, resolveRef));
|
|
1429
|
+
}
|
|
1430
|
+
out.push(`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
1431
|
+
return out;
|
|
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
|
+
};
|
|
1455
|
+
const lines = [];
|
|
1456
|
+
const closers = [];
|
|
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 : ""}) ──`);
|
|
1458
|
+
let indent = " ";
|
|
1459
|
+
if (m.parent !== undefined && priorNodeAt(m.parent, atPos)) {
|
|
1460
|
+
lines.push(`${indent}if produced_${sanitize(m.parent)}.get() {`);
|
|
1461
|
+
closers.unshift(`${indent}}`);
|
|
1462
|
+
indent += " ";
|
|
1463
|
+
}
|
|
1464
|
+
lines.push(`${indent}let over_${s} = ${overExpr};`);
|
|
1465
|
+
lines.push(`${indent}let mut built_${s}: Vec<${elemName}> = Vec::with_capacity(over_${s}.len());`);
|
|
1466
|
+
if (batched) {
|
|
1467
|
+
// build kept-element ports (+ kept over elements for into). Batched into is never guarded (rejected).
|
|
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());`);
|
|
1471
|
+
lines.push(`${indent}for oel_${s} in over_${s}.iter() {`);
|
|
1472
|
+
for (const l of emitGuard(indent + " "))
|
|
1473
|
+
lines.push(l);
|
|
1474
|
+
for (const l of buildPorts(indent + " "))
|
|
1475
|
+
lines.push(l);
|
|
1476
|
+
lines.push(`${indent} items_${s}.push(ep_${s});`);
|
|
1477
|
+
if (hasInto)
|
|
1478
|
+
lines.push(`${indent} kept_${s}.push(oel_${s}.clone());`);
|
|
1479
|
+
lines.push(`${indent}}`);
|
|
1480
|
+
lines.push(`${indent}if !items_${s}.is_empty() {`);
|
|
1481
|
+
lines.push(`${indent} let want_${s} = items_${s}.len();`);
|
|
1482
|
+
lines.push(`${indent} let bp_${s} = ${batch} { items: items_${s} };`);
|
|
1483
|
+
lines.push(`${indent} let row_${s} = match handlers.${nodeMethodName(node.id)}(&bp_${s}, None)${awaitSfx} {`);
|
|
1484
|
+
lines.push(`${indent} Some(r) => r,`);
|
|
1485
|
+
lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(m.component)})),`);
|
|
1486
|
+
lines.push(`${indent} };`);
|
|
1487
|
+
lines.push(`${indent} if row_${s}.is_error {`);
|
|
1488
|
+
lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
|
|
1489
|
+
lines.push(`${indent} }`);
|
|
1490
|
+
lines.push(`${indent} if row_${s}.rows.len() != want_${s} {`);
|
|
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})));`);
|
|
1492
|
+
lines.push(`${indent} }`);
|
|
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");`);
|
|
1498
|
+
lines.push(`${indent} built_${s}.push(${elemMat}(over_el_${s}, er_${s}));`);
|
|
1499
|
+
lines.push(`${indent} }`);
|
|
1500
|
+
}
|
|
1501
|
+
else {
|
|
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() {`);
|
|
1504
|
+
lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
|
|
1505
|
+
lines.push(`${indent} built_${s}.push(el_${s});`);
|
|
1506
|
+
lines.push(`${indent} }`);
|
|
1507
|
+
}
|
|
1508
|
+
lines.push(`${indent}}`);
|
|
1509
|
+
}
|
|
1510
|
+
else {
|
|
1511
|
+
// #93 shape 2 / #108: NON-batched map — dispatch ONCE PER KEPT ELEMENT.
|
|
1512
|
+
lines.push(`${indent}for oel_${s} in over_${s}.iter() {`);
|
|
1513
|
+
for (const l of emitGuard(indent + " "))
|
|
1514
|
+
lines.push(l);
|
|
1515
|
+
for (const l of buildPorts(indent + " "))
|
|
1516
|
+
lines.push(l);
|
|
1517
|
+
lines.push(`${indent} let er_${s} = match handlers.${nodeMethodName(node.id)}(&ep_${s}, None)${awaitSfx} {`);
|
|
1518
|
+
lines.push(`${indent} Some(r) => r,`);
|
|
1519
|
+
lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(m.component)})),`);
|
|
1520
|
+
lines.push(`${indent} };`);
|
|
1521
|
+
lines.push(`${indent} if er_${s}.is_error {`);
|
|
1522
|
+
lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(node.id)}, er_${s}.err)));`);
|
|
1523
|
+
lines.push(`${indent} }`);
|
|
1524
|
+
if (hasInto) {
|
|
1525
|
+
lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s}, er_${s}));`);
|
|
1526
|
+
}
|
|
1527
|
+
else {
|
|
1528
|
+
lines.push(`${indent} let _ = &oel_${s};`);
|
|
1529
|
+
lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
|
|
1530
|
+
lines.push(`${indent} built_${s}.push(el_${s});`);
|
|
1531
|
+
}
|
|
1532
|
+
lines.push(`${indent}}`);
|
|
1533
|
+
}
|
|
1534
|
+
lines.push(`${indent}*${typedCell(node.id)}.borrow_mut() = built_${s};`);
|
|
1535
|
+
lines.push(`${indent}produced_${sanitize(node.id)}.set(true);`);
|
|
1536
|
+
for (const c of closers)
|
|
1537
|
+
lines.push(c);
|
|
1538
|
+
return lines.join("\n");
|
|
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
|
+
}
|
|
1605
|
+
/** emitElemMove — move a concrete element row (RawElemNR, error already handled) into a fresh `let` local
|
|
1606
|
+
* of the element outType. Named → struct literal from moved fields; scalar/arr/opt → .val. */
|
|
1607
|
+
function emitElemMove(letBind, rowExpr, ref, plan) {
|
|
1608
|
+
if (ref.kind === "named") {
|
|
1609
|
+
const decl = findDecl(plan, ref.name);
|
|
1610
|
+
const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: ${rowExpr}.${rustFieldName(f.name)}`).join(", ");
|
|
1611
|
+
return `${letBind} = ${ref.name} { ${inits} };`;
|
|
1612
|
+
}
|
|
1613
|
+
return `${letBind} = ${rowExpr}.val;`;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* emitProducedAwareValue — the #86 part 1 produced-aware output lowering (rust twin of the go one).
|
|
1617
|
+
*/
|
|
1618
|
+
function emitProducedAwareValue(node, expected, typedNodes, plan, skippable, skippableKind) {
|
|
1619
|
+
const e = classifyExpr(node);
|
|
1620
|
+
if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
|
|
1621
|
+
const head = e.path[0];
|
|
1622
|
+
const baseRef = typedNodes.get(head);
|
|
1623
|
+
if (skippable.has(head)) {
|
|
1624
|
+
const base = `${typedCell(head)}.borrow().clone()`;
|
|
1625
|
+
const { expr, ref } = typedFieldAccess(base, baseRef, e.path.slice(1), plan);
|
|
1626
|
+
const producedExpr = valueLeafProduced(expr, ref, expected);
|
|
1627
|
+
const isConn = skippableKind.get(head) === "connection" && e.path.length === 1;
|
|
1628
|
+
let unproduced;
|
|
1629
|
+
if (isConn) {
|
|
1630
|
+
const connTy = expected && expected.kind === "opt" ? renderTypeRef(expected.inner) : renderTypeRef(expected ?? ref);
|
|
1631
|
+
unproduced = expected && expected.kind === "opt" ? `Some(${connTy}::default())` : `${connTy}::default()`;
|
|
1632
|
+
}
|
|
1633
|
+
else {
|
|
1634
|
+
unproduced = expected && expected.kind === "opt" ? "None" : "Default::default()";
|
|
1635
|
+
}
|
|
1636
|
+
return {
|
|
1637
|
+
expr: `if produced_${sanitize(head)}.get() { ${producedExpr} } else { ${unproduced} }`,
|
|
1638
|
+
ref: expected ?? ref,
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
const isBareCopyScalar = e.path.length === 1 && baseRef.kind === "scalar" && (baseRef.scalar === "int" || baseRef.scalar === "float" || baseRef.scalar === "bool");
|
|
1642
|
+
const base = isBareCopyScalar ? `*${typedCell(head)}.borrow()` : `${typedCell(head)}.borrow().clone()`;
|
|
1643
|
+
const { expr, ref } = typedFieldAccess(base, baseRef, e.path.slice(1), plan);
|
|
1644
|
+
return { expr, ref };
|
|
1645
|
+
}
|
|
1646
|
+
if (node !== null && typeof node === "object" && !Array.isArray(node)) {
|
|
1647
|
+
const keys = Object.keys(node);
|
|
1648
|
+
if (keys.length === 1 && keys[0] === "obj" && expected && expected.kind === "named") {
|
|
1649
|
+
const decl = findDecl(plan, expected.name);
|
|
1650
|
+
const fields = node.obj;
|
|
1651
|
+
const inits = decl.fields.map((f) => {
|
|
1652
|
+
const sub = emitProducedAwareValue(fields[f.name], f.type, typedNodes, plan, skippable, skippableKind);
|
|
1653
|
+
return `${rustFieldName(f.name)}: ${sub.expr}`;
|
|
1654
|
+
});
|
|
1655
|
+
return { expr: `${expected.name} { ${inits.join(", ")} }`, ref: expected };
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return emitTypedValue(node, expected, typedNodes, plan);
|
|
1659
|
+
}
|
|
1660
|
+
function valueLeafProduced(expr, ref, expected) {
|
|
1661
|
+
if (expected && expected.kind === "opt") {
|
|
1662
|
+
if (ref.kind === "opt")
|
|
1663
|
+
return expr;
|
|
1664
|
+
return `Some(${expr})`;
|
|
1665
|
+
}
|
|
1666
|
+
return expr;
|
|
1667
|
+
}
|
|
1668
|
+
// bc#90 / runtime-free: the old `valueLeafInner` (which serialized a bindField to a boxed `Value` for the
|
|
1669
|
+
// binding check) is REMOVED — the bound value is now a typed `Option<K>` built by direct field access
|
|
1670
|
+
// (Some(field.clone()) / the opt field's Option), and the produced gate is `Option::is_some()`.
|
|
1671
|
+
// ── struct dispatch surface (STRUCT-only — no Value bind, no serializer) ─────────────────
|
|
1672
|
+
function emitStructSurface(native) {
|
|
1673
|
+
const names = native.map((c) => rustStrLit(c.name));
|
|
1674
|
+
return `// COMPONENT_NAMES_NATIVE_RAW — covered reads exposed on the combined struct-native path. Each is
|
|
1675
|
+
// driven via run_native_raw_struct_<comp>(handlers, in_) -> Result<T, BehaviorError>: a STRUCT return
|
|
1676
|
+
// (the consumer builds the CONCRETE InNR_<comp> input + implements the CONCRETE HandlerNR<comp> trait).
|
|
1677
|
+
// See INTEGRATION.md §6.
|
|
1678
|
+
pub const COMPONENT_NAMES_NATIVE_RAW: [&str; ${names.length}] = [${names.join(", ")}];`;
|
|
1679
|
+
}
|
|
1680
|
+
// ── unknown_component helper (self-contained; the module carries no straight-line body) ──
|
|
1681
|
+
const UNKNOWN_COMPONENT_HELPER = `#[allow(dead_code)]
|
|
1682
|
+
fn unknown_component(component: &str) -> BehaviorError {
|
|
1683
|
+
BehaviorError::new("UNKNOWN_COMPONENT", format!("component '{component}' has no handler (fail-closed)"))
|
|
1684
|
+
}`;
|
|
1685
|
+
// ── module assembly ──────────────────────────────────────────────────────────────────
|
|
1686
|
+
function emit(ctx) {
|
|
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);
|
|
1692
|
+
for (const c of ctx.ir.components)
|
|
1693
|
+
assertTypedCoverage(c);
|
|
1694
|
+
const native = ctx.ir.components.filter(isNativeRaw);
|
|
1695
|
+
const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
|
|
1696
|
+
// FAIL CLOSED (bc#86/#89): the --typed-native (native codegen) surface is native-only. Previously a
|
|
1697
|
+
// non-covered component was SILENTLY DROPPED (emitted only as a comment note), concealing that it
|
|
1698
|
+
// never got native codegen. We now throw a LOUD GeneratorFailure naming every uncovered component +
|
|
1699
|
+
// its specific reject reason. A fully-covered module is unchanged.
|
|
1700
|
+
if (nonNative.length > 0) {
|
|
1701
|
+
const rejects = nonNative
|
|
1702
|
+
.map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
|
|
1703
|
+
.join("\n");
|
|
1704
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (native codegen) surface: ${nonNative.length} component(s) are NOT a covered native read:\n${rejects}\n\n` +
|
|
1705
|
+
`The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
|
|
1706
|
+
`box uncovered shapes. Implement native coverage for this shape, fix the spec if the shape is ` +
|
|
1707
|
+
`genuinely runtime-dynamic, or generate this component on the --ir dynamic interpreter surface.`);
|
|
1708
|
+
}
|
|
1709
|
+
if (native.length === 0) {
|
|
1710
|
+
// an empty native surface (no covered read) carries no runner and no failure — nothing references
|
|
1711
|
+
// bc-runtime, so it stays runtime-free (bc#90) with only the component-names const.
|
|
1712
|
+
return `#![allow(dead_code, unused_imports)]
|
|
1713
|
+
${emitStructSurface([])}
|
|
1714
|
+
`;
|
|
1715
|
+
}
|
|
1716
|
+
const plan = buildTypePlan(ctx.ir);
|
|
1717
|
+
// The typed-native module is a de-boxed CONSUMER artifact: the covered runner RETURNS these outType
|
|
1718
|
+
// structs and the consumer reads their fields natively. So the decls (and their FIELDS) are pub — the
|
|
1719
|
+
// consumer keeps the model native (mirrors Go's exported outType struct fields on the covered path).
|
|
1720
|
+
const decls = emitTypeDecls(plan)
|
|
1721
|
+
.replace(/\nstruct /g, "\npub struct ")
|
|
1722
|
+
.replace(/^struct /gm, "pub struct ")
|
|
1723
|
+
.replace(/^( {4})([a-z_][A-Za-z0-9_]*: )/gm, "$1pub $2")
|
|
1724
|
+
.replace(/^( {4})(r#[a-z_][A-Za-z0-9_]*: )/gm, "$1pub $2");
|
|
1725
|
+
const defaults = emitDefaults(plan);
|
|
1726
|
+
// #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler).
|
|
1727
|
+
rustExprUsed.helpers = false;
|
|
1728
|
+
const structs = [];
|
|
1729
|
+
const rowStructs = [];
|
|
1730
|
+
const handlerTraits = [];
|
|
1731
|
+
const inStructs = [];
|
|
1732
|
+
const elemMaterializers = [];
|
|
1733
|
+
for (const c of native) {
|
|
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));
|
|
1738
|
+
for (const n of c.body) {
|
|
1739
|
+
if ("map" in 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
|
+
}
|
|
1744
|
+
else
|
|
1745
|
+
structs.push(emitPortsStruct(c, n));
|
|
1746
|
+
}
|
|
1747
|
+
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
1748
|
+
handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
|
|
1749
|
+
inStructs.push(emitInStruct(c, plan));
|
|
1750
|
+
const mm = emitMapElemMaterializers(c, typedNodes, plan);
|
|
1751
|
+
if (mm)
|
|
1752
|
+
elemMaterializers.push(mm);
|
|
1753
|
+
}
|
|
1754
|
+
const runners = native.map((c) => emitNativeRawRunner(c, plan, asyncPlans.get(c.name))).join("\n\n");
|
|
1755
|
+
const structSurface = emitStructSurface(native);
|
|
1756
|
+
// (nonNative.length === 0 is guaranteed here — the fail-closed guard above threw otherwise.)
|
|
1757
|
+
const banner = `#![allow(dead_code, unused_imports, non_snake_case)]
|
|
1758
|
+
// GENERATED by behavior-contracts (bc#90, rust-typed-native — the RUNTIME-FREE read de-box; STRUCT-ONLY,
|
|
1759
|
+
// FULLY-NATIVE). A covered read is a strictly-sequential typed componentRef chain (point reads +
|
|
1760
|
+
// relationKind:single children), emitted ONLY as a STRUCT-returning runner GENERIC over a per-component
|
|
1761
|
+
// CONCRETE handler trait (HandlerNR<comp>): native ports struct IN (direct construction — every port is a
|
|
1762
|
+
// CONCRETE Rust value: String / bool / Vec<&str> projection / i64 limit — NO boxed Value, NO by-name
|
|
1763
|
+
// accessor); the handler takes a typed produced-aware 'bound' Option and returns the node's CONCRETE row
|
|
1764
|
+
// struct (typed fields = the projected outType), and the runner reads row.<field> DIRECTLY into the node's
|
|
1765
|
+
// outType struct. The covered plane carries NO bc-runtime reference at all — failures use the LOCAL
|
|
1766
|
+
// BehaviorError (same codes, byte-equal to run_behavior). INLINE sequential (no plan driver) so a relation
|
|
1767
|
+
// child reads the parent's REAL struct result via direct field access and the child-present decision is
|
|
1768
|
+
// made from the real parent value — relationSingle / connection converge with run_behavior (fixes
|
|
1769
|
+
// #323/#74). The exposed API is run_native_raw_struct_<comp>(handlers, in_) over the CONCRETE InNR_<comp>
|
|
1770
|
+
// input struct + the CONCRETE HandlerNR<comp> trait (the consumer builds/implements both, keeping the
|
|
1771
|
+
// model native). This module is FULLY NATIVE: a naive grep for boxing primitives OR for the bc-runtime
|
|
1772
|
+
// crate finds nothing, by design (bc#90 / runtime-free).
|
|
1773
|
+
use std::cell::RefCell;`;
|
|
1774
|
+
const sections = [
|
|
1775
|
+
banner,
|
|
1776
|
+
`// Local concrete failure type (runtime-free) — a covered runner returns Result<T, BehaviorError> over\n// THIS local type instead of a bc-runtime failure, so the fully-covered module imports ZERO bc runtime.\n${emitBehaviorErrorType()}`,
|
|
1777
|
+
decls || undefined,
|
|
1778
|
+
defaults || undefined,
|
|
1779
|
+
UNKNOWN_COMPONENT_HELPER,
|
|
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,
|
|
1789
|
+
`// CONCRETE per-component input structs (fields = inputPorts).\n${inStructs.join("\n\n")}`,
|
|
1790
|
+
`// CONCRETE per-component handler traits (one node_* method per node — native ports IN, row OUT).\n${handlerTraits.join("\n\n")}`,
|
|
1791
|
+
elemMaterializers.length
|
|
1792
|
+
? `// map augmented-element copiers (pure struct field assignment — no dynamic value read).\n${elemMaterializers.join("\n\n")}`
|
|
1793
|
+
: undefined,
|
|
1794
|
+
`// Combined read runners (STRUCT-returning — the fully de-plumbed CONCRETE path).\n${runners}`,
|
|
1795
|
+
structSurface,
|
|
1796
|
+
];
|
|
1797
|
+
return `${sections.filter((x) => !!x).join("\n\n")}\n`;
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* rustTypedNativeObserve — the TEST-ONLY observe companion (a submodule string the test harness writes
|
|
1801
|
+
* alongside the covered module). It carries the typed->Value serializers (ser_*) + a CONCRETE scripted
|
|
1802
|
+
* handler (ScriptedNativeRaw) that implements every HandlerNR<comp> trait by decoding a scripted Value
|
|
1803
|
+
* into the concrete per-node row, + an observe_native_raw dispatch that decodes the generic input into the
|
|
1804
|
+
* concrete InNR_<comp>, drives the module-private struct runner, and serializes its result to a canonical
|
|
1805
|
+
* Value for the equivalence pin. Lives OUTSIDE the covered module so the module stays literal whole-file
|
|
1806
|
+
* zero (the ser_* / decode helpers are test glue, never on the consumer's read hot path).
|
|
1807
|
+
*/
|
|
1808
|
+
export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
1809
|
+
const components = ir.components;
|
|
1810
|
+
const native = components.filter(isNativeRaw);
|
|
1811
|
+
const plan = buildTypePlan(ir);
|
|
1812
|
+
const needSync = native.some((c) => concurrencyPlan(c) !== null);
|
|
1813
|
+
const serializers = emitSerializers(plan);
|
|
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);
|
|
1820
|
+
// ── per-component input decoders: generic &[(String,Value)] -> concrete InNR_<comp> (test glue). ──
|
|
1821
|
+
const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
|
|
1822
|
+
// ── concrete scripted handler: implements every HandlerNR<comp> trait by decoding a scripted Value
|
|
1823
|
+
// into the concrete per-node row (the generic scripted-source queue meets the concrete seam). ──
|
|
1824
|
+
const decodeFns = [];
|
|
1825
|
+
const emittedDecode = new Set();
|
|
1826
|
+
const traitImpls = [];
|
|
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");
|
|
1831
|
+
const typedNodes = new Map();
|
|
1832
|
+
for (const n of c.body)
|
|
1833
|
+
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1834
|
+
const methods = [];
|
|
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.
|
|
1838
|
+
const ref = typedNodes.get(n.id);
|
|
1839
|
+
const fnKw = fnKwFor(n.id);
|
|
1840
|
+
// bc#90 / runtime-free: the scripted impl's `_bound` param mirrors the trait's typed produced-aware
|
|
1841
|
+
// Option<K> (NOT a boxed Value) — computed per node so the impl signature matches the trait.
|
|
1842
|
+
const bt = boundRustType(n, typedNodes, plan);
|
|
1843
|
+
if ("map" in n) {
|
|
1844
|
+
const m = n.map;
|
|
1845
|
+
const batched = m.batched === true;
|
|
1846
|
+
const arrRef = ref;
|
|
1847
|
+
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
1848
|
+
const elemT = rawElemStructName(c.name, n.id);
|
|
1849
|
+
const batchT = rawRowStructName(c.name, n.id);
|
|
1850
|
+
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
1851
|
+
const portsT = batched ? `${portsStructName(c.name, n.id)}Batch` : portsStructName(c.name, n.id);
|
|
1852
|
+
const retT = batched ? batchT : elemT;
|
|
1853
|
+
if (batched) {
|
|
1854
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
|
|
1855
|
+
let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
|
|
1856
|
+
if is_err {
|
|
1857
|
+
return Some(${batchT} { is_error: true, err, ..Default::default() });
|
|
1858
|
+
}
|
|
1859
|
+
let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
|
|
1860
|
+
let rows = arr.iter().map(${elemDecode}).collect();
|
|
1861
|
+
Some(${batchT} { rows, ..Default::default() })
|
|
1862
|
+
}`);
|
|
1863
|
+
}
|
|
1864
|
+
else {
|
|
1865
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${elemT}> {
|
|
1866
|
+
let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
|
|
1867
|
+
if is_err {
|
|
1868
|
+
return Some(${elemT} { is_error: true, err, ..Default::default() });
|
|
1869
|
+
}
|
|
1870
|
+
let v = ok.unwrap_or(Value::Null);
|
|
1871
|
+
Some(${elemDecode}(&v))
|
|
1872
|
+
}`);
|
|
1873
|
+
}
|
|
1874
|
+
void retT;
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
const node = n;
|
|
1878
|
+
const rowT = rawRowStructName(c.name, node.id);
|
|
1879
|
+
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
1880
|
+
methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, _ports: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
|
|
1881
|
+
let (is_err, err, ok) = self.next(${rustStrLit(node.component)})?;
|
|
1882
|
+
if is_err {
|
|
1883
|
+
return Some(${rowT} { is_error: true, err, ..Default::default() });
|
|
1884
|
+
}
|
|
1885
|
+
let v = ok.unwrap_or(Value::Null);
|
|
1886
|
+
Some(${decode}(&v))
|
|
1887
|
+
}`);
|
|
1888
|
+
}
|
|
1889
|
+
traitImpls.push(`impl ${handlerTraitName(c.name)} for ScriptedNativeRaw {\n${methods.join("\n")}\n}`);
|
|
1890
|
+
}
|
|
1891
|
+
const arms = native
|
|
1892
|
+
.map((c) => {
|
|
1893
|
+
const outRef = c.outputType !== undefined ? deriveTypeRef(c.outputType, plan) : undefined;
|
|
1894
|
+
const ser = outRef ? serializeTyped("tv", outRef, plan) : "tv";
|
|
1895
|
+
// bc#90 / runtime-free: the covered runner returns the module-LOCAL `BehaviorError` (via super::*);
|
|
1896
|
+
// bridge it to the bc-runtime `behavior_contracts::BehaviorError` at the observe boundary (same
|
|
1897
|
+
// code/message) so the conformance harness (main.rs) reads the stable code without the covered
|
|
1898
|
+
// module ever touching bc runtime. decode_* is test glue and returns the local error too.
|
|
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 }`;
|
|
1905
|
+
})
|
|
1906
|
+
.join("\n");
|
|
1907
|
+
// the scripted handler is passed as `&ScriptedNativeRaw`; the observe entry takes `&ScriptedNativeRaw`
|
|
1908
|
+
// directly (the harness constructs it). Sync is inherent (the queues live behind a Mutex, seen behind
|
|
1909
|
+
// an atomic) so a concurrency stage runs it in parallel.
|
|
1910
|
+
const adapter = `// ScriptedNativeRaw — the CONCRETE test handler: it satisfies every HandlerNR<comp> trait by draining a
|
|
1911
|
+
// per-COMPONENT scripted-outcome queue and decoding the scripted ok Value into the concrete row struct.
|
|
1912
|
+
// The decode (decode_row_*) is generated (it knows the concrete row type), so the runner stays fully
|
|
1913
|
+
// concrete. Sync (queues behind a Mutex, the probe counter an atomic) so a bc#87 stage parallelizes it.
|
|
1914
|
+
pub struct ScriptedNativeRaw {
|
|
1915
|
+
queues: std::sync::Mutex<std::collections::HashMap<String, (Vec<serde_json::Value>, bool)>>,
|
|
1916
|
+
#[allow(dead_code)]
|
|
1917
|
+
seen: std::sync::atomic::AtomicUsize,
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
impl ScriptedNativeRaw {
|
|
1921
|
+
// build from the JSON handlers spec: { "<component>": {ok|error} | [ {ok|error}, … ] }.
|
|
1922
|
+
pub fn new(spec: &serde_json::Map<String, serde_json::Value>) -> Self {
|
|
1923
|
+
let mut queues = std::collections::HashMap::new();
|
|
1924
|
+
for (name, raw) in spec {
|
|
1925
|
+
let (items, single) = match raw {
|
|
1926
|
+
serde_json::Value::Array(a) => (a.clone(), false),
|
|
1927
|
+
other => (vec![other.clone()], true),
|
|
1928
|
+
};
|
|
1929
|
+
queues.insert(name.clone(), (items, single));
|
|
1930
|
+
}
|
|
1931
|
+
ScriptedNativeRaw {
|
|
1932
|
+
queues: std::sync::Mutex::new(queues),
|
|
1933
|
+
seen: std::sync::atomic::AtomicUsize::new(0),
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// drain one scripted outcome for a component (last entry repeats — mirrors the reference scripted
|
|
1938
|
+
// handlers); None when the component has no scripted queue (fail-closed). Returns (is_error, err,
|
|
1939
|
+
// ok_value). The bc#87 parallel-stage runner calls node_* from multiple threads, so the drain is
|
|
1940
|
+
// Mutex-guarded.
|
|
1941
|
+
fn next(&self, component: &str) -> Option<(bool, String, Option<Value>)> {
|
|
1942
|
+
self.seen.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
1943
|
+
let mut queues = self.queues.lock().unwrap();
|
|
1944
|
+
let (queue, single) = queues.get_mut(component)?;
|
|
1945
|
+
let raw = if *single || queue.len() <= 1 { queue[0].clone() } else { queue.remove(0) };
|
|
1946
|
+
if let Some(okv) = raw.get("ok") {
|
|
1947
|
+
let v = behavior_contracts::decode_value(okv).expect("decode ok");
|
|
1948
|
+
Some((false, String::new(), Some(v)))
|
|
1949
|
+
} else {
|
|
1950
|
+
let e = raw.get("error").and_then(|e| e.as_str()).unwrap_or("").to_string();
|
|
1951
|
+
Some((true, e, None))
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}`;
|
|
1955
|
+
const observeBound = needSync ? "" : "";
|
|
1956
|
+
void observeBound;
|
|
1957
|
+
return `// GENERATED test-only observe companion (bc#77/#94) — NOT part of the covered module. Serializes the
|
|
1958
|
+
// struct-native runner's result to a canonical Value for the equivalence pin, and carries the CONCRETE
|
|
1959
|
+
// scripted test handler (ScriptedNativeRaw) that satisfies every HandlerNR<comp> trait by decoding a
|
|
1960
|
+
// scripted Value into the concrete per-node row. The consumer never uses this (it keeps the model native
|
|
1961
|
+
// + implements HandlerNR<comp> over its own wire payload); it exists only so the test harness can
|
|
1962
|
+
// byte-compare the STRUCT-only covered read against run_behavior. A submodule of the covered module
|
|
1963
|
+
// (reads its pub run_native_raw_struct_* + the CONCRETE structs/traits — incl. the LOCAL BehaviorError —
|
|
1964
|
+
// via super::*). bc#90 / runtime-free: the covered module's BehaviorError is LOCAL, so this test-glue
|
|
1965
|
+
// companion refers to it via super::* (as \`BehaviorError\`) and refers to the bc-runtime failure type
|
|
1966
|
+
// FULLY-QUALIFIED (\`behavior_contracts::BehaviorError\`) only where it bridges at the observe boundary.
|
|
1967
|
+
#![allow(dead_code, unused_imports)]
|
|
1968
|
+
use behavior_contracts::Value;
|
|
1969
|
+
use super::*;
|
|
1970
|
+
|
|
1971
|
+
// bridge the covered module's LOCAL BehaviorError (super::BehaviorError) to the bc-runtime failure type
|
|
1972
|
+
// the conformance harness reads (same code + message) — the ONLY place the two meet (test glue).
|
|
1973
|
+
fn to_rt_err(e: BehaviorError) -> behavior_contracts::BehaviorError {
|
|
1974
|
+
behavior_contracts::BehaviorError::new(e.code, e.message)
|
|
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
|
+
: ""}
|
|
2000
|
+
|
|
2001
|
+
${serializers}
|
|
2002
|
+
|
|
2003
|
+
// Value -> typed struct marshallers for the concrete-row decode (TEST glue only).
|
|
2004
|
+
${marshallers}
|
|
2005
|
+
|
|
2006
|
+
${emitMarshalHelpers()}
|
|
2007
|
+
|
|
2008
|
+
// per-node concrete-row decoders (scripted Value -> RawRowNR_/RawElemNR_ struct).
|
|
2009
|
+
${decodeFns.join("\n\n")}
|
|
2010
|
+
|
|
2011
|
+
// input decoders (generic &[(String,Value)] -> concrete InNR_<comp>; TEST glue, one decode at the boundary).
|
|
2012
|
+
${inDecoders}
|
|
2013
|
+
|
|
2014
|
+
${adapter}
|
|
2015
|
+
|
|
2016
|
+
// scripted handler trait impls (one per covered component — the concrete node_* seam, test glue).
|
|
2017
|
+
${traitImpls.join("\n\n")}
|
|
2018
|
+
|
|
2019
|
+
// observe_native_raw drives the CONCRETE struct-native runner and serializes to a canonical Value (test
|
|
2020
|
+
// only). Takes the CONCRETE &ScriptedNativeRaw handler so each enclosed run_native_raw_struct_* call is
|
|
2021
|
+
// instantiated at the concrete handler type (direct per-node method calls), not at a trait object.
|
|
2022
|
+
pub fn observe_native_raw(
|
|
2023
|
+
name: &str,
|
|
2024
|
+
handlers: &ScriptedNativeRaw,
|
|
2025
|
+
input: &[(String, Value)],
|
|
2026
|
+
) -> Result<Value, behavior_contracts::BehaviorError> {
|
|
2027
|
+
match name {
|
|
2028
|
+
${arms}
|
|
2029
|
+
other => Err(behavior_contracts::BehaviorError::new("UNKNOWN_ENTRY", format!("component '{other}' is not a covered read (fail-closed)"))),
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
`;
|
|
2033
|
+
}
|
|
2034
|
+
/** emitInDecoder — a TEST-glue decoder `decode_InNR_<comp>(input) -> Result<InNR_<comp>, BehaviorError>`.
|
|
2035
|
+
* Reads each declared input port from the generic &[(String,Value)] into the concrete field (scalar →
|
|
2036
|
+
* typed read; Value-typed → the raw Value clone). A missing key leaves Default (every covered input is
|
|
2037
|
+
* required + supplied by the corpus). Lives ONLY in the observe companion. */
|
|
2038
|
+
function emitInDecoder(comp, plan) {
|
|
2039
|
+
const name = inStructName(comp.name);
|
|
2040
|
+
const ports = Object.keys(comp.inputPorts ?? {});
|
|
2041
|
+
const lines = [];
|
|
2042
|
+
lines.push(`fn decode_${name}(input: &[(String, Value)]) -> Result<${name}, BehaviorError> {`);
|
|
2043
|
+
lines.push(` let mut in_ = ${name}::default();`);
|
|
2044
|
+
if (ports.length === 0 || comp.inputPorts === undefined) {
|
|
2045
|
+
lines.push(` let _ = input;`);
|
|
2046
|
+
lines.push(` Ok(in_)`);
|
|
2047
|
+
lines.push(`}`);
|
|
2048
|
+
return lines.join("\n");
|
|
2049
|
+
}
|
|
2050
|
+
// an if/else-if chain (clippy-clean regardless of port count — a single-arm `match` trips single_match).
|
|
2051
|
+
lines.push(` for (k, v) in input {`);
|
|
2052
|
+
ports.forEach((p, i) => {
|
|
2053
|
+
const field = rustFieldName(p);
|
|
2054
|
+
const schema = comp.inputPorts[p];
|
|
2055
|
+
const scalar = inputScalarKind(schema);
|
|
2056
|
+
const et = schema.elemType;
|
|
2057
|
+
const head = i === 0 ? " if" : " } else if";
|
|
2058
|
+
lines.push(`${head} k == ${rustStrLit(p)} {`);
|
|
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) {
|
|
2074
|
+
lines.push(` in_.${field} = v.clone();`);
|
|
2075
|
+
}
|
|
2076
|
+
else {
|
|
2077
|
+
const pat = scalar === "String" ? "Value::Str(sv)" : scalar === "i64" ? "Value::Int(sv)" : scalar === "f64" ? "Value::Float(sv)" : "Value::Bool(sv)";
|
|
2078
|
+
const label = scalar === "String" ? "string" : scalar === "i64" ? "int" : scalar === "f64" ? "float" : "bool";
|
|
2079
|
+
const bind = scalar === "String" ? "sv.clone()" : "*sv";
|
|
2080
|
+
// bc#90 / runtime-free: build the module-LOCAL BehaviorError (TYPE_MISMATCH) via the local
|
|
2081
|
+
// marshal_type_error helper — decode_* returns the local error, bridged at the observe boundary.
|
|
2082
|
+
lines.push(` match v { ${pat} => in_.${field} = ${bind}, other => return Err(marshal_type_error(${rustStrLit(`decode input ${p}: expected ${label}, got `)}, other.type_name())) }`);
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
lines.push(` }`);
|
|
2086
|
+
lines.push(` }`);
|
|
2087
|
+
lines.push(` Ok(in_)`);
|
|
2088
|
+
lines.push(`}`);
|
|
2089
|
+
return lines.join("\n");
|
|
2090
|
+
}
|
|
2091
|
+
/** emitDecodeRow — emit (once, deduped) a decoder `decode_row_<comp>_<node><suffix>(v: &Value) -> <rowT>`
|
|
2092
|
+
* that materializes the scripted ok Value into the concrete row struct. Named: reuse the Value->struct
|
|
2093
|
+
* marshaller + move fields; scalar/arr/opt: materialize into .val. TEST glue only. */
|
|
2094
|
+
function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix) {
|
|
2095
|
+
const fn = `decode_row_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}${suffix.toLowerCase()}`;
|
|
2096
|
+
if (emitted.has(fn))
|
|
2097
|
+
return fn;
|
|
2098
|
+
emitted.add(fn);
|
|
2099
|
+
const lines = [];
|
|
2100
|
+
lines.push(`fn ${fn}(v: &Value) -> ${rowT} {`);
|
|
2101
|
+
if (ref.kind === "named") {
|
|
2102
|
+
const decl = findDecl(plan, ref.name);
|
|
2103
|
+
lines.push(` let t = marshal_${ref.name}(v).expect("scripted row decode");`);
|
|
2104
|
+
const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: t.${rustFieldName(f.name)}`).join(", ");
|
|
2105
|
+
lines.push(` ${rowT} { ${inits}, ..Default::default() }`);
|
|
2106
|
+
}
|
|
2107
|
+
else {
|
|
2108
|
+
const mat = materializeExpr("v", ref, plan);
|
|
2109
|
+
lines.push(` let val = (|| -> Result<${renderTypeRef(ref)}, BehaviorError> { Ok(${mat}) })().expect("scripted row decode");`);
|
|
2110
|
+
lines.push(` ${rowT} { val, ..Default::default() }`);
|
|
2111
|
+
}
|
|
2112
|
+
lines.push(`}`);
|
|
2113
|
+
out.push(lines.join("\n"));
|
|
2114
|
+
return fn;
|
|
2115
|
+
}
|
|
2116
|
+
export const rustTypedNativeEmitter = {
|
|
2117
|
+
language: "rust-typed-native",
|
|
2118
|
+
fileExtension: ".rs",
|
|
2119
|
+
defaultRuntimeImport: "behavior_contracts",
|
|
2120
|
+
filenameHint: "behaviors_typed_native_generated.rs",
|
|
2121
|
+
emit,
|
|
2122
|
+
};
|
|
2123
|
+
//# sourceMappingURL=emit-typed-native-rust.js.map
|