behavior-contracts 0.3.0 → 0.5.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/generator/core.d.ts +14 -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-raw-abi-go.d.ts.map +1 -1
- package/dist/generator/emit-raw-abi-go.js +2 -1
- package/dist/generator/emit-raw-abi-go.js.map +1 -1
- package/dist/generator/emit-straightline-go.d.ts +2 -0
- package/dist/generator/emit-straightline-go.d.ts.map +1 -1
- package/dist/generator/emit-straightline-go.js +37 -19
- package/dist/generator/emit-straightline-go.js.map +1 -1
- package/dist/generator/emit-straightline-native-go.d.ts +54 -0
- package/dist/generator/emit-straightline-native-go.d.ts.map +1 -0
- package/dist/generator/emit-straightline-native-go.js +835 -0
- package/dist/generator/emit-straightline-native-go.js.map +1 -0
- package/dist/generator/emit-straightline-native-rust.d.ts +27 -0
- package/dist/generator/emit-straightline-native-rust.d.ts.map +1 -0
- package/dist/generator/emit-straightline-native-rust.js +624 -0
- package/dist/generator/emit-straightline-native-rust.js.map +1 -0
- package/dist/generator/emit-straightline-rust.d.ts.map +1 -1
- package/dist/generator/emit-straightline-rust.js +5 -12
- package/dist/generator/emit-straightline-rust.js.map +1 -1
- package/dist/generator/emit-straightline-typed-go.d.ts +17 -0
- package/dist/generator/emit-straightline-typed-go.d.ts.map +1 -1
- package/dist/generator/emit-straightline-typed-go.js +4 -1
- package/dist/generator/emit-straightline-typed-go.js.map +1 -1
- package/dist/generator/emit-straightline-typed-rust.d.ts +16 -0
- package/dist/generator/emit-straightline-typed-rust.d.ts.map +1 -1
- package/dist/generator/emit-straightline-typed-rust.js +2 -0
- package/dist/generator/emit-straightline-typed-rust.js.map +1 -1
- package/dist/generator/emit-straightline-typescript.d.ts.map +1 -1
- package/dist/generator/emit-straightline-typescript.js +4 -10
- package/dist/generator/emit-straightline-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 +1840 -0
- package/dist/generator/emit-typed-native-go.js.map +1 -0
- package/dist/generator/emit-typed-native-rust.d.ts +52 -0
- package/dist/generator/emit-typed-native-rust.d.ts.map +1 -0
- package/dist/generator/emit-typed-native-rust.js +1673 -0
- package/dist/generator/emit-typed-native-rust.js.map +1 -0
- package/dist/generator/index.d.ts +2 -0
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +16 -0
- package/dist/generator/index.js.map +1 -1
- 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/package.json +1 -1
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import { rustStraightlineEmitter, rustStraightlineInternals } from "./emit-straightline-rust.js";
|
|
2
|
+
import { sequentialOrder, analyzeSequentialOps, classifyExpr } from "./straightline.js";
|
|
3
|
+
const { P, sanitize, rustStrLit } = rustStraightlineInternals;
|
|
4
|
+
/** Rust struct field name for a wire port (snake-ish, deterministic, valid ident). */
|
|
5
|
+
function rustFieldName(wire) {
|
|
6
|
+
const safe = wire.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase();
|
|
7
|
+
return /^[a-z_]/.test(safe) ? `f_${safe}` : `f_${safe}`;
|
|
8
|
+
}
|
|
9
|
+
/** Rust struct type name for a node (PascalCase-ish, module-unique). */
|
|
10
|
+
function portsStructName(compName, nodeId) {
|
|
11
|
+
return `Ports${pascal(compName)}${pascal(nodeId)}`;
|
|
12
|
+
}
|
|
13
|
+
function pascal(s) {
|
|
14
|
+
return s.replace(/[^A-Za-z0-9]+/g, " ").split(" ").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") || "X";
|
|
15
|
+
}
|
|
16
|
+
function inferPortType(node, inputPorts) {
|
|
17
|
+
// A static arr port is a Vec<Value> carried in a boxed Value struct field (Value::Arr) — still
|
|
18
|
+
// no vec!/generic key-value container; built by a direct array-to-Vec assignment.
|
|
19
|
+
if (staticArrElems(node) !== null)
|
|
20
|
+
return "Value";
|
|
21
|
+
const e = classifyExpr(node);
|
|
22
|
+
switch (e.kind) {
|
|
23
|
+
case "str":
|
|
24
|
+
case "concat":
|
|
25
|
+
return "String";
|
|
26
|
+
case "bool":
|
|
27
|
+
return "bool";
|
|
28
|
+
case "ref": {
|
|
29
|
+
if (e.path.length === 1) {
|
|
30
|
+
const s = inputPorts[e.path[0]];
|
|
31
|
+
if (s) {
|
|
32
|
+
if (s.type === "string")
|
|
33
|
+
return "String";
|
|
34
|
+
if (s.type === "int")
|
|
35
|
+
return "i64";
|
|
36
|
+
if (s.type === "float")
|
|
37
|
+
return "f64";
|
|
38
|
+
if (s.type === "bool")
|
|
39
|
+
return "bool";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return "Value";
|
|
43
|
+
}
|
|
44
|
+
default:
|
|
45
|
+
return "Value";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* staticArrElems — if `node` is a single-key `{arr:[...]}` shape, return its element nodes; else
|
|
50
|
+
* null. classifyExpr keeps `arr` as dynamic (its construction lives in the primitive SSoT), so we
|
|
51
|
+
* recognize the STATIC arr shape here and build it as a Vec<Value> WITHOUT the `vec!` macro (the
|
|
52
|
+
* purity gate bans `vec![`) — via `[...].into()` on a fixed-size array of Values.
|
|
53
|
+
*/
|
|
54
|
+
function staticArrElems(node) {
|
|
55
|
+
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
56
|
+
return null;
|
|
57
|
+
const keys = Object.keys(node);
|
|
58
|
+
if (keys.length !== 1 || keys[0] !== "arr")
|
|
59
|
+
return null;
|
|
60
|
+
const arg = node.arr;
|
|
61
|
+
return Array.isArray(arg) ? arg : null;
|
|
62
|
+
}
|
|
63
|
+
function isStaticallyResolvable(node) {
|
|
64
|
+
const arr = staticArrElems(node);
|
|
65
|
+
if (arr !== null)
|
|
66
|
+
return arr.every(isStaticallyResolvable);
|
|
67
|
+
const e = classifyExpr(node);
|
|
68
|
+
switch (e.kind) {
|
|
69
|
+
case "str":
|
|
70
|
+
case "bool":
|
|
71
|
+
case "null":
|
|
72
|
+
case "ref":
|
|
73
|
+
return true;
|
|
74
|
+
case "concat":
|
|
75
|
+
return e.parts.every((p) => isStaticallyResolvable(reconstruct(p)));
|
|
76
|
+
default:
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function reconstruct(e) {
|
|
81
|
+
switch (e.kind) {
|
|
82
|
+
case "str":
|
|
83
|
+
return e.value;
|
|
84
|
+
case "bool":
|
|
85
|
+
return e.value;
|
|
86
|
+
case "null":
|
|
87
|
+
return null;
|
|
88
|
+
case "ref":
|
|
89
|
+
return { [e.opt ? "refOpt" : "ref"]: e.path };
|
|
90
|
+
case "concat":
|
|
91
|
+
return { concat: e.parts.map(reconstruct) };
|
|
92
|
+
case "dynamic":
|
|
93
|
+
return e.node;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function isNativeComponent(comp) {
|
|
97
|
+
if (sequentialOrder(comp) === null)
|
|
98
|
+
return false;
|
|
99
|
+
for (const n of comp.body) {
|
|
100
|
+
if (!("component" in n) || "map" in n || "cond" in n)
|
|
101
|
+
return false;
|
|
102
|
+
const cr = n;
|
|
103
|
+
if (cr.bindField !== undefined)
|
|
104
|
+
return false;
|
|
105
|
+
if (cr.relationKind !== undefined)
|
|
106
|
+
return false;
|
|
107
|
+
if (cr.policy !== undefined && cr.policy !== "fail")
|
|
108
|
+
return false;
|
|
109
|
+
for (const p of Object.keys(cr.ports))
|
|
110
|
+
if (!isStaticallyResolvable(cr.ports[p]))
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return isStaticallyResolvable(comp.output);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* isNativeMapComponent — a component whose body is a SINGLE `map` node is native-eligible when
|
|
117
|
+
* over is a ref, every per-element port is statically resolvable, no when/into, default 'fail'
|
|
118
|
+
* policy, no relationKind, and the output is statically resolvable. Mirrors the Go predicate;
|
|
119
|
+
* emitNativeMapRunner reproduces emitRSeqMap's control flow with native element ports structs.
|
|
120
|
+
*/
|
|
121
|
+
function isNativeMapComponent(comp) {
|
|
122
|
+
if (sequentialOrder(comp) === null)
|
|
123
|
+
return false;
|
|
124
|
+
if (comp.body.length !== 1)
|
|
125
|
+
return false;
|
|
126
|
+
const n = comp.body[0];
|
|
127
|
+
if (!("map" in n))
|
|
128
|
+
return false;
|
|
129
|
+
const m = n.map;
|
|
130
|
+
if (m.when !== undefined)
|
|
131
|
+
return false;
|
|
132
|
+
if (m.into !== undefined)
|
|
133
|
+
return false;
|
|
134
|
+
if (m.relationKind !== undefined)
|
|
135
|
+
return false;
|
|
136
|
+
if (m.policy !== undefined && m.policy !== "fail")
|
|
137
|
+
return false;
|
|
138
|
+
if (classifyExpr(m.over).kind !== "ref")
|
|
139
|
+
return false;
|
|
140
|
+
for (const p of Object.keys(m.ports))
|
|
141
|
+
if (!isStaticallyResolvable(m.ports[p]))
|
|
142
|
+
return false;
|
|
143
|
+
return isStaticallyResolvable(comp.output);
|
|
144
|
+
}
|
|
145
|
+
/** A native component (sequential componentRef OR single map-write). */
|
|
146
|
+
function isNative(comp) {
|
|
147
|
+
return isNativeComponent(comp) || isNativeMapComponent(comp);
|
|
148
|
+
}
|
|
149
|
+
/** The `Value` clone expr for a port field when read via PortReader (box one field on demand). */
|
|
150
|
+
function portToValue(field, ty) {
|
|
151
|
+
switch (ty) {
|
|
152
|
+
case "String":
|
|
153
|
+
return `Value::Str(self.${field}.clone())`;
|
|
154
|
+
case "i64":
|
|
155
|
+
return `Value::Int(self.${field})`;
|
|
156
|
+
case "f64":
|
|
157
|
+
return `Value::Float(self.${field})`;
|
|
158
|
+
case "bool":
|
|
159
|
+
return `Value::Bool(self.${field})`;
|
|
160
|
+
case "Value":
|
|
161
|
+
return `self.${field}.clone()`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function emitPortsStruct(comp, node) {
|
|
165
|
+
const name = portsStructName(comp.name, node.id);
|
|
166
|
+
const portNames = Object.keys(node.ports);
|
|
167
|
+
if (portNames.length === 0) {
|
|
168
|
+
return `// ${name} — native ports for node '${node.id}' (component ${node.component}); no ports.
|
|
169
|
+
struct ${name};
|
|
170
|
+
impl behavior_contracts::PortReader for ${name} {
|
|
171
|
+
fn port(&self, _name: &str) -> Option<Value> { None }
|
|
172
|
+
fn as_any(&self) -> &dyn std::any::Any { self }
|
|
173
|
+
}`;
|
|
174
|
+
}
|
|
175
|
+
const types = portNames.map((p) => inferPortType(node.ports[p], comp.inputPorts));
|
|
176
|
+
const fields = portNames
|
|
177
|
+
.map((p, i) => ` ${rustFieldName(p)}: ${types[i] === "Value" ? "Value" : types[i]}, // ${JSON.stringify(p)}`)
|
|
178
|
+
.join("\n");
|
|
179
|
+
const arms = portNames
|
|
180
|
+
.map((p, i) => ` ${rustStrLit(p)} => Some(${portToValue(rustFieldName(p), types[i])}),`)
|
|
181
|
+
.join("\n");
|
|
182
|
+
return `// ${name} — NATIVE ports for node '${node.id}' (component ${node.component}). Typed fields per the
|
|
183
|
+
// static port type; constructed directly (no Vec, no heap key strings, no per-port Value boxing).
|
|
184
|
+
struct ${name} {
|
|
185
|
+
${fields}
|
|
186
|
+
}
|
|
187
|
+
impl behavior_contracts::PortReader for ${name} {
|
|
188
|
+
fn port(&self, name: &str) -> Option<Value> {
|
|
189
|
+
match name {
|
|
190
|
+
${arms}
|
|
191
|
+
_ => None,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
fn as_any(&self) -> &dyn std::any::Any { self }
|
|
195
|
+
}`;
|
|
196
|
+
}
|
|
197
|
+
/** Emit an `R`-typed expr (Result<Value,ExprFailure>) for a static ref/concat/literal, reading
|
|
198
|
+
* prior node results from their locals (nr_<id>) or input params from `input`. */
|
|
199
|
+
function emitStaticR(node, atPos, priorNodeAt, nodeResult) {
|
|
200
|
+
const arr = staticArrElems(node);
|
|
201
|
+
if (arr !== null) {
|
|
202
|
+
// Static arr → Ok(Value::Arr([e0?, e1?, …].into())). Uses a fixed-size array + `.into()` to a
|
|
203
|
+
// Vec — NO `vec!` macro (purity gate). Each element unwraps via `?` (ExprFailure propagates).
|
|
204
|
+
if (arr.length === 0)
|
|
205
|
+
return `Ok::<Value, behavior_contracts::ExprFailure>(Value::Arr(Vec::new()))`;
|
|
206
|
+
const elems = arr.map((el) => `(${emitStaticR(el, atPos, priorNodeAt, nodeResult)})?`).join(", ");
|
|
207
|
+
return `Ok::<Value, behavior_contracts::ExprFailure>(Value::Arr([${elems}].into()))`;
|
|
208
|
+
}
|
|
209
|
+
const e = classifyExpr(node);
|
|
210
|
+
switch (e.kind) {
|
|
211
|
+
case "str":
|
|
212
|
+
return `${P}::str_native(${rustStrLit(e.value)})`;
|
|
213
|
+
case "bool":
|
|
214
|
+
return `${P}::bool_native(${e.value ? "true" : "false"})`;
|
|
215
|
+
case "null":
|
|
216
|
+
return `${P}::null_native()`;
|
|
217
|
+
case "ref": {
|
|
218
|
+
const head = e.path[0];
|
|
219
|
+
const fn = e.opt ? "ref_opt_native" : "ref_native";
|
|
220
|
+
if (priorNodeAt(head, atPos)) {
|
|
221
|
+
// Read the prior node's NATIVE result local via a 1-key borrowed scope (no persistent Vec).
|
|
222
|
+
const path = e.path.map(rustStrLit).join(", ");
|
|
223
|
+
return `${P}::${fn}(&[${path}], &[(${rustStrLit(head)}.to_string(), ${nodeResult(head)}.clone())])`;
|
|
224
|
+
}
|
|
225
|
+
const path = e.path.map(rustStrLit).join(", ");
|
|
226
|
+
return `${P}::${fn}(&[${path}], input)`;
|
|
227
|
+
}
|
|
228
|
+
case "concat": {
|
|
229
|
+
const parts = e.parts.map((p) => `(${emitStaticR(reconstruct(p), atPos, priorNodeAt, nodeResult)})?`).join(", ");
|
|
230
|
+
return `${P}::concat_native(&[${parts}])`;
|
|
231
|
+
}
|
|
232
|
+
default:
|
|
233
|
+
throw new Error("rust native: non-static expr reached emitStaticR (guarded by isNativeComponent)");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function emitNativeRunner(comp) {
|
|
237
|
+
const order = sequentialOrder(comp);
|
|
238
|
+
const metas = analyzeSequentialOps(
|
|
239
|
+
// reuse the shared plan model for policy analysis (opsLiteral shape)
|
|
240
|
+
(function () {
|
|
241
|
+
// minimal opsLiteral mirror: id + parent + policy (native components have no bindField/map/cond)
|
|
242
|
+
const idToIndex = new Map();
|
|
243
|
+
comp.body.forEach((n, i) => idToIndex.set(n.id, i));
|
|
244
|
+
return comp.body.map((n) => {
|
|
245
|
+
const cr = n;
|
|
246
|
+
const pid = cr.parent;
|
|
247
|
+
return {
|
|
248
|
+
id: n.id,
|
|
249
|
+
parent: pid !== undefined ? idToIndex.get(pid) ?? null : null,
|
|
250
|
+
policy: cr.policy,
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
})(), order);
|
|
254
|
+
const fn = sanitize(comp.name);
|
|
255
|
+
const nodeResult = (id) => `nr_${id.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
256
|
+
const idToPos = new Map();
|
|
257
|
+
order.forEach((opIdx, k) => idToPos.set(comp.body[opIdx].id, k));
|
|
258
|
+
const priorNodeAt = (head, atPos) => {
|
|
259
|
+
const p = idToPos.get(head);
|
|
260
|
+
return p !== undefined && p < atPos;
|
|
261
|
+
};
|
|
262
|
+
// Does any port expr OR the output read an INPUT param (a ref whose head is not a prior node)?
|
|
263
|
+
// If not, the `input` parameter is unused — bind it as `_input` to stay clippy-clean.
|
|
264
|
+
const readsInput = (() => {
|
|
265
|
+
const walk = (node, atPos) => {
|
|
266
|
+
const e = classifyExpr(node);
|
|
267
|
+
if (e.kind === "ref")
|
|
268
|
+
return !priorNodeAt(e.path[0], atPos);
|
|
269
|
+
if (e.kind === "concat")
|
|
270
|
+
return e.parts.some((p) => walk(reconstruct(p), atPos));
|
|
271
|
+
return false;
|
|
272
|
+
};
|
|
273
|
+
for (const k of order.keys()) {
|
|
274
|
+
const cr = comp.body[order[k]];
|
|
275
|
+
if (Object.values(cr.ports).some((pn) => walk(pn, k)))
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
return walk(comp.output, order.length);
|
|
279
|
+
})();
|
|
280
|
+
const inputParam = readsInput ? "input" : "_input";
|
|
281
|
+
const lines = [];
|
|
282
|
+
lines.push(`// run_native_${fn} — NATIVE-PORTS straight-line exec (bc 0.5.0): each componentRef constructs a`);
|
|
283
|
+
lines.push(`// native ports struct directly (no vec!/to_string key/Value boxing) and dispatches via`);
|
|
284
|
+
lines.push(`// exec_native (NativeComponentExec). Node results are native locals; cross-node refs read them (#74).`);
|
|
285
|
+
lines.push(`fn run_native_${fn}<H: behavior_contracts::NativeComponentExec>(`);
|
|
286
|
+
lines.push(` handlers: &mut H,`);
|
|
287
|
+
lines.push(` ${inputParam}: &[(String, Value)],`);
|
|
288
|
+
lines.push(`) -> Result<Value, BehaviorError> {`);
|
|
289
|
+
for (const k of order.keys()) {
|
|
290
|
+
const i = order[k];
|
|
291
|
+
const node = comp.body[i];
|
|
292
|
+
const meta = metas[i];
|
|
293
|
+
const structName = portsStructName(comp.name, node.id);
|
|
294
|
+
const portNames = Object.keys(node.ports);
|
|
295
|
+
lines.push(` // ── op '${node.id}' (${node.component}) ──`);
|
|
296
|
+
const inits = [];
|
|
297
|
+
for (const pn of portNames) {
|
|
298
|
+
const expr = node.ports[pn];
|
|
299
|
+
const field = rustFieldName(pn);
|
|
300
|
+
const ty = inferPortType(expr, comp.inputPorts);
|
|
301
|
+
const rExpr = emitStaticR(expr, k, priorNodeAt, nodeResult);
|
|
302
|
+
const local = `pv_${sanitize(node.id)}_${pn.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
303
|
+
lines.push(` let ${local}: Value = (${rExpr}).map_err(BehaviorError::from)?;`);
|
|
304
|
+
if (ty === "Value") {
|
|
305
|
+
inits.push(`${field}: ${local}`);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// typed field: pattern-extract the concrete scalar (fail-closed TYPE_MISMATCH otherwise).
|
|
309
|
+
const pat = ty === "String" ? "Value::Str(s)" : ty === "i64" ? "Value::Int(s)" : ty === "f64" ? "Value::Float(s)" : "Value::Bool(s)";
|
|
310
|
+
const label = ty === "String" ? "string" : ty === "i64" ? "int" : ty === "f64" ? "float" : "bool";
|
|
311
|
+
lines.push(` let ${local}_t = match ${local} { ${pat} => s, ref other => return Err(behavior_contracts::raw_type_mismatch(${rustStrLit(label)}, other.type_name())) };`);
|
|
312
|
+
inits.push(`${field}: ${local}_t`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
lines.push(` let ports_${sanitize(node.id)} = ${structName} { ${inits.join(", ")} };`);
|
|
316
|
+
lines.push(` let outcome_${sanitize(node.id)} = match handlers.exec_native_ctx(${rustStrLit(node.id)}, ${rustStrLit(node.component)}, &ports_${sanitize(node.id)}, None) {`);
|
|
317
|
+
lines.push(` Some(o) => o,`);
|
|
318
|
+
lines.push(` None => return Err(unknown_component(${rustStrLit(node.component)})),`);
|
|
319
|
+
lines.push(` };`);
|
|
320
|
+
if (meta.policy === "continue") {
|
|
321
|
+
lines.push(` let ${nodeResult(node.id)}: Value = match outcome_${sanitize(node.id)} { behavior_contracts::ExecOutcome::Ok(v) => v, behavior_contracts::ExecOutcome::Error(_) => Value::Null };`);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const label = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
|
|
325
|
+
lines.push(` let ${nodeResult(node.id)}: Value = match outcome_${sanitize(node.id)} {`);
|
|
326
|
+
lines.push(` behavior_contracts::ExecOutcome::Ok(v) => v,`);
|
|
327
|
+
lines.push(` behavior_contracts::ExecOutcome::Error(e) => return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under '${label}: {}", ${rustStrLit(node.id)}, e))),`);
|
|
328
|
+
lines.push(` };`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const outR = emitStaticR(comp.output, order.length, priorNodeAt, nodeResult);
|
|
332
|
+
lines.push(` (${outR}).map_err(BehaviorError::from)`);
|
|
333
|
+
lines.push(`}`);
|
|
334
|
+
return lines.join("\n");
|
|
335
|
+
}
|
|
336
|
+
/** emitMapStaticR — R-typed expr for a map element port: a ref whose head is the element binding
|
|
337
|
+
* (`as`) resolves to the element local via a 1-key borrowed scope; other refs read `input`. */
|
|
338
|
+
function emitMapStaticR(node, asName, elLocal) {
|
|
339
|
+
const arr = staticArrElems(node);
|
|
340
|
+
if (arr !== null) {
|
|
341
|
+
if (arr.length === 0)
|
|
342
|
+
return `Ok::<Value, behavior_contracts::ExprFailure>(Value::Arr(Vec::new()))`;
|
|
343
|
+
const elems = arr.map((el) => `(${emitMapStaticR(el, asName, elLocal)})?`).join(", ");
|
|
344
|
+
return `Ok::<Value, behavior_contracts::ExprFailure>(Value::Arr([${elems}].into()))`;
|
|
345
|
+
}
|
|
346
|
+
const e = classifyExpr(node);
|
|
347
|
+
switch (e.kind) {
|
|
348
|
+
case "str":
|
|
349
|
+
return `${P}::str_native(${rustStrLit(e.value)})`;
|
|
350
|
+
case "bool":
|
|
351
|
+
return `${P}::bool_native(${e.value ? "true" : "false"})`;
|
|
352
|
+
case "null":
|
|
353
|
+
return `${P}::null_native()`;
|
|
354
|
+
case "ref": {
|
|
355
|
+
const head = e.path[0];
|
|
356
|
+
const fn = e.opt ? "ref_opt_native" : "ref_native";
|
|
357
|
+
const path = e.path.map(rustStrLit).join(", ");
|
|
358
|
+
if (head === asName) {
|
|
359
|
+
return `${P}::${fn}(&[${path}], &[(${rustStrLit(head)}.to_string(), ${elLocal}.clone())])`;
|
|
360
|
+
}
|
|
361
|
+
return `${P}::${fn}(&[${path}], input)`;
|
|
362
|
+
}
|
|
363
|
+
case "concat": {
|
|
364
|
+
const parts = e.parts.map((p) => `(${emitMapStaticR(reconstruct(p), asName, elLocal)})?`).join(", ");
|
|
365
|
+
return `${P}::concat_native(&[${parts}])`;
|
|
366
|
+
}
|
|
367
|
+
default:
|
|
368
|
+
throw new Error("rust native: non-static map port expr reached emitMapStaticR");
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* emitNativeMapRunner — NATIVE-PORTS exec for a single-map-write component (rust). Reproduces
|
|
373
|
+
* emitRSeqMap's control flow (over→Value::Arr match, MAP_OVER_NOT_ARRAY, per-element loop,
|
|
374
|
+
* batched vs per-element dispatch, kept/collected, MAP_BATCH_RESULT_MISMATCH) but each element's
|
|
375
|
+
* ports are a native struct (direct construction, no vec!/key-string/Value boxing) dispatched via
|
|
376
|
+
* exec_native_ctx. The batched dispatch passes a native batch struct holding the Vec<ElemStruct>
|
|
377
|
+
* (a Vec<StructName>, NOT the banned Vec<(String,Value)>); a consumer downcasts via as_any().
|
|
378
|
+
*/
|
|
379
|
+
function emitNativeMapRunner(comp) {
|
|
380
|
+
const fn = sanitize(comp.name);
|
|
381
|
+
const m = comp.body[0].map;
|
|
382
|
+
const id = comp.body[0].id;
|
|
383
|
+
const s = sanitize(id);
|
|
384
|
+
const structName = portsStructName(comp.name, id);
|
|
385
|
+
const asName = m.as;
|
|
386
|
+
const batched = m.batched === true;
|
|
387
|
+
const comp_ = rustStrLit(m.component);
|
|
388
|
+
const nodeId = rustStrLit(id);
|
|
389
|
+
const overE = classifyExpr(m.over);
|
|
390
|
+
const overHead = overE.kind === "ref" ? overE.path[0] : "";
|
|
391
|
+
const portNames = Object.keys(m.ports);
|
|
392
|
+
const fieldTypes = portNames.map((p) => inferPortType(m.ports[p], comp.inputPorts));
|
|
393
|
+
const lines = [];
|
|
394
|
+
lines.push(`// run_native_${fn} — NATIVE-PORTS single-map-write exec (bc 0.5.0): reproduces the generic map`);
|
|
395
|
+
lines.push(`// control flow, but each element's ports are a native struct (direct construction, no vec!)`);
|
|
396
|
+
lines.push(`// dispatched via exec_native_ctx (${batched ? "batched" : "per-element"}).`);
|
|
397
|
+
lines.push(`fn run_native_${fn}<H: behavior_contracts::NativeComponentExec>(`);
|
|
398
|
+
lines.push(` handlers: &mut H,`);
|
|
399
|
+
lines.push(` input: &[(String, Value)],`);
|
|
400
|
+
lines.push(`) -> Result<Value, BehaviorError> {`);
|
|
401
|
+
// over evaluation + array match (matches emitRSeqMap).
|
|
402
|
+
lines.push(` let over_${s}: Vec<Value> = match (${P}::ref_native(&[${rustStrLit(overHead)}], input)).map_err(BehaviorError::from)? {`);
|
|
403
|
+
lines.push(` Value::Arr(a) => a,`);
|
|
404
|
+
lines.push(` _ => return Err(BehaviorError::new("MAP_OVER_NOT_ARRAY", "map '${id}': 'over' did not evaluate to an array".to_string())),`);
|
|
405
|
+
lines.push(` };`);
|
|
406
|
+
lines.push(` let mut kept_${s}: Vec<usize> = Vec::new();`);
|
|
407
|
+
const buildElem = (ind) => {
|
|
408
|
+
const out = [];
|
|
409
|
+
const inits = [];
|
|
410
|
+
for (let pi = 0; pi < portNames.length; pi++) {
|
|
411
|
+
const pn = portNames[pi];
|
|
412
|
+
const field = rustFieldName(pn);
|
|
413
|
+
const ty = fieldTypes[pi];
|
|
414
|
+
const rExpr = emitMapStaticR(m.ports[pn], asName, `el_${s}`);
|
|
415
|
+
const local = `pv_${s}_${pn.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
416
|
+
out.push(`${ind}let ${local}: Value = (${rExpr}).map_err(BehaviorError::from)?;`);
|
|
417
|
+
if (ty === "Value") {
|
|
418
|
+
inits.push(`${field}: ${local}`);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
const pat = ty === "String" ? "Value::Str(s)" : ty === "i64" ? "Value::Int(s)" : ty === "f64" ? "Value::Float(s)" : "Value::Bool(s)";
|
|
422
|
+
const label = ty === "String" ? "string" : ty === "i64" ? "int" : ty === "f64" ? "float" : "bool";
|
|
423
|
+
out.push(`${ind}let ${local}_t = match ${local} { ${pat} => s, ref other => return Err(behavior_contracts::raw_type_mismatch(${rustStrLit(label)}, other.type_name())) };`);
|
|
424
|
+
inits.push(`${field}: ${local}_t`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
out.push(`${ind}let ports_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
428
|
+
return out;
|
|
429
|
+
};
|
|
430
|
+
if (batched) {
|
|
431
|
+
const batchStruct = `${structName}Batch`;
|
|
432
|
+
lines.push(` let mut items_${s}: Vec<${structName}> = Vec::new();`);
|
|
433
|
+
lines.push(` for (el_${s}_idx, el_${s}) in over_${s}.iter().enumerate() {`);
|
|
434
|
+
lines.push(...buildElem(` `));
|
|
435
|
+
lines.push(` items_${s}.push(ports_${s});`, ` kept_${s}.push(el_${s}_idx);`, ` }`);
|
|
436
|
+
lines.push(` let mut collected_${s}: Vec<Value> = Vec::new();`);
|
|
437
|
+
lines.push(` if !items_${s}.is_empty() {`);
|
|
438
|
+
lines.push(` let want_${s} = items_${s}.len();`);
|
|
439
|
+
lines.push(` let bp_${s} = ${batchStruct} { items: items_${s} };`);
|
|
440
|
+
lines.push(` match handlers.exec_native_ctx(${nodeId}, ${comp_}, &bp_${s}, None) {`);
|
|
441
|
+
lines.push(` None => return Err(unknown_component(${comp_})),`);
|
|
442
|
+
lines.push(` Some(behavior_contracts::ExecOutcome::Error(e)) => return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${nodeId}, e))),`);
|
|
443
|
+
lines.push(` Some(behavior_contracts::ExecOutcome::Ok(v)) => match v {`);
|
|
444
|
+
lines.push(` Value::Arr(r) if r.len() == want_${s} => { collected_${s} = r; }`);
|
|
445
|
+
lines.push(` _ => return Err(BehaviorError::new("MAP_BATCH_RESULT_MISMATCH", format!("map '${id}': batched handler must return a list aligned to items (want {want_${s}})"))),`);
|
|
446
|
+
lines.push(` },`);
|
|
447
|
+
lines.push(` }`);
|
|
448
|
+
lines.push(` }`);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
lines.push(` let mut collected_${s}: Vec<Value> = Vec::new();`);
|
|
452
|
+
lines.push(` for (el_${s}_idx, el_${s}) in over_${s}.iter().enumerate() {`);
|
|
453
|
+
lines.push(...buildElem(` `));
|
|
454
|
+
lines.push(` match handlers.exec_native_ctx(${nodeId}, ${comp_}, &ports_${s}, Some(el_${s})) {`);
|
|
455
|
+
lines.push(` None => return Err(unknown_component(${comp_})),`);
|
|
456
|
+
lines.push(` Some(behavior_contracts::ExecOutcome::Error(e)) => return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${nodeId}, e))),`);
|
|
457
|
+
lines.push(` Some(behavior_contracts::ExecOutcome::Ok(v)) => { collected_${s}.push(v); kept_${s}.push(el_${s}_idx); }`);
|
|
458
|
+
lines.push(` }`);
|
|
459
|
+
lines.push(` }`);
|
|
460
|
+
}
|
|
461
|
+
lines.push(` let _ = &kept_${s};`);
|
|
462
|
+
lines.push(` let nr_${s}: Value = Value::Arr(collected_${s});`);
|
|
463
|
+
// output: a ref to the map node result (or a static arr wrapper). Resolve via a 1-key scope.
|
|
464
|
+
const priorNodeAt = (head) => head === id;
|
|
465
|
+
const nodeResult = () => `nr_${s}`;
|
|
466
|
+
const outR = emitStaticRMapOut(comp.output, id, `nr_${s}`, priorNodeAt, nodeResult);
|
|
467
|
+
lines.push(` (${outR}).map_err(BehaviorError::from)`);
|
|
468
|
+
lines.push(`}`);
|
|
469
|
+
return lines.join("\n");
|
|
470
|
+
}
|
|
471
|
+
/** Resolve the map-write output (a ref to the node result / static arr wrapping it) to an R expr,
|
|
472
|
+
* reading the map node id from its native local via a 1-key borrowed scope. */
|
|
473
|
+
function emitStaticRMapOut(node, id, local, priorNodeAt, _nodeResult) {
|
|
474
|
+
const arr = staticArrElems(node);
|
|
475
|
+
if (arr !== null) {
|
|
476
|
+
if (arr.length === 0)
|
|
477
|
+
return `Ok::<Value, behavior_contracts::ExprFailure>(Value::Arr(Vec::new()))`;
|
|
478
|
+
const elems = arr.map((el) => `(${emitStaticRMapOut(el, id, local, priorNodeAt, _nodeResult)})?`).join(", ");
|
|
479
|
+
return `Ok::<Value, behavior_contracts::ExprFailure>(Value::Arr([${elems}].into()))`;
|
|
480
|
+
}
|
|
481
|
+
const e = classifyExpr(node);
|
|
482
|
+
switch (e.kind) {
|
|
483
|
+
case "str":
|
|
484
|
+
return `${P}::str_native(${rustStrLit(e.value)})`;
|
|
485
|
+
case "bool":
|
|
486
|
+
return `${P}::bool_native(${e.value ? "true" : "false"})`;
|
|
487
|
+
case "null":
|
|
488
|
+
return `${P}::null_native()`;
|
|
489
|
+
case "ref": {
|
|
490
|
+
const head = e.path[0];
|
|
491
|
+
const fn = e.opt ? "ref_opt_native" : "ref_native";
|
|
492
|
+
const path = e.path.map(rustStrLit).join(", ");
|
|
493
|
+
if (head === id) {
|
|
494
|
+
return `${P}::${fn}(&[${path}], &[(${rustStrLit(head)}.to_string(), ${local}.clone())])`;
|
|
495
|
+
}
|
|
496
|
+
return `${P}::${fn}(&[${path}], input)`;
|
|
497
|
+
}
|
|
498
|
+
case "concat": {
|
|
499
|
+
const parts = e.parts.map((p) => `(${emitStaticRMapOut(reconstruct(p), id, local, priorNodeAt, _nodeResult)})?`).join(", ");
|
|
500
|
+
return `${P}::concat_native(&[${parts}])`;
|
|
501
|
+
}
|
|
502
|
+
default:
|
|
503
|
+
throw new Error("rust native: non-static map output reached emitStaticRMapOut");
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/** Emit the element ports struct + PortReader, plus a batch ports struct (when batched). */
|
|
507
|
+
function emitMapPortsStructs(comp) {
|
|
508
|
+
const m = comp.body[0].map;
|
|
509
|
+
const id = comp.body[0].id;
|
|
510
|
+
const structName = portsStructName(comp.name, id);
|
|
511
|
+
const elemStruct = emitPortsStruct(comp, { id, component: m.component, ports: m.ports });
|
|
512
|
+
if (m.batched !== true)
|
|
513
|
+
return elemStruct;
|
|
514
|
+
const batchStruct = `${structName}Batch`;
|
|
515
|
+
return `${elemStruct}
|
|
516
|
+
|
|
517
|
+
// ${batchStruct} — NATIVE batched ports for map '${id}': holds the Vec of per-element native ports
|
|
518
|
+
// structs (a Vec<${structName}>, NOT the banned generic Vec<(String,Value)> container). A consumer's
|
|
519
|
+
// batched handler downcasts via as_any() to read the typed element ports; port() returns None (the
|
|
520
|
+
// element structs are not individually Value-boxable — the de-box is the whole point).
|
|
521
|
+
struct ${batchStruct} {
|
|
522
|
+
items: Vec<${structName}>,
|
|
523
|
+
}
|
|
524
|
+
impl behavior_contracts::PortReader for ${batchStruct} {
|
|
525
|
+
fn port(&self, _name: &str) -> Option<Value> { None }
|
|
526
|
+
fn as_any(&self) -> &dyn std::any::Any { self }
|
|
527
|
+
}`;
|
|
528
|
+
}
|
|
529
|
+
function emitBindNative(ctx) {
|
|
530
|
+
const native = ctx.ir.components.filter(isNative);
|
|
531
|
+
const arms = native.map((c) => ` ${rustStrLit(c.name)} => run_native_${sanitize(c.name)}(&mut self.handlers, input),`).join("\n");
|
|
532
|
+
const names = native.map((c) => rustStrLit(c.name)).join(", ");
|
|
533
|
+
return `// bind_native — the NATIVE-PORTS dispatch surface (bc 0.5.0): binds a NativeComponentExec
|
|
534
|
+
// (native ports struct + boxed Value result) to the de-plumbed runners. Only native-covered
|
|
535
|
+
// components are exposed; a consumer keeps the rest on the generic bind. Additive.
|
|
536
|
+
pub fn bind_native<H: behavior_contracts::NativeComponentExec>(handlers: H) -> BoundNative<H> {
|
|
537
|
+
BoundNative { handlers }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
pub struct BoundNative<H: behavior_contracts::NativeComponentExec> {
|
|
541
|
+
handlers: H,
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
impl<H: behavior_contracts::NativeComponentExec> BoundNative<H> {
|
|
545
|
+
pub fn call(&mut self, name: &str, input: &[(String, Value)]) -> Result<Value, BehaviorError> {
|
|
546
|
+
match name {
|
|
547
|
+
${arms}
|
|
548
|
+
other => Err(BehaviorError::new("UNKNOWN_ENTRY", format!("component '{other}' not found in this module (fail-closed)"))),
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// COMPONENT_NAMES_NATIVE — components exposed on the native-ports path (declaration order).
|
|
554
|
+
pub const COMPONENT_NAMES_NATIVE: &[&str] = &[${names}];`;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* emit — a native-eligible component gets ONLY the native run_native_ / bind_native path; the
|
|
558
|
+
* generic key-value run_ / bind is emitted ONLY for components that are NOT native-eligible
|
|
559
|
+
* (map/cond/concurrency). A fully-native module therefore contains ZERO generic port container
|
|
560
|
+
* (no `vec![(... .to_string(), Value ...)]`, no generic exec_ctx on the port plane) — the owner
|
|
561
|
+
* purity requirement + grep gate. We drive the generic rust emitter with a FILTERED context
|
|
562
|
+
* (non-native components only).
|
|
563
|
+
*/
|
|
564
|
+
function emit(ctx) {
|
|
565
|
+
const native = ctx.ir.components.filter(isNative);
|
|
566
|
+
const nonNative = ctx.ir.components.filter((c) => !isNative(c));
|
|
567
|
+
const filteredCtx = {
|
|
568
|
+
...ctx,
|
|
569
|
+
ir: { ...ctx.ir, components: nonNative },
|
|
570
|
+
componentNames: nonNative.map((c) => c.name),
|
|
571
|
+
};
|
|
572
|
+
let straightlineCode = rustStraightlineEmitter.emit(filteredCtx);
|
|
573
|
+
// A native module still carries the generic body FOR THE NON-NATIVE components (bind/Bound/
|
|
574
|
+
// run_ fns); when the module has native components, the generic `bind` is dead only if every
|
|
575
|
+
// component is native (then the generic body is component-empty). Allow dead_code at module
|
|
576
|
+
// level so the carried generic surface + the native surface coexist clippy-clean regardless of
|
|
577
|
+
// which the consumer drives (mirrors the raw-abi emitter).
|
|
578
|
+
// When a native module carries the generic scaffold ONLY for non-native components (or, in a
|
|
579
|
+
// fully-native module, an EMPTY generic bind/Bound/dispatch with no component arms), that dead
|
|
580
|
+
// scaffold trips dead_code / unused_imports / unused_variables / match_single_binding. Allow
|
|
581
|
+
// them at module level: the scaffold is legitimately dead there, and — crucially — it holds NO
|
|
582
|
+
// generic key-value port container (no vec!/to_string-key/boxed-Value), which is what the owner
|
|
583
|
+
// purity gate forbids. The native surface below is the live, de-plumbed path.
|
|
584
|
+
straightlineCode = straightlineCode.replace("#![allow(non_snake_case)]", "#![allow(non_snake_case)]\n#![allow(dead_code, unused_imports, unused_variables, clippy::match_single_binding)]");
|
|
585
|
+
if (native.length === 0) {
|
|
586
|
+
return `${straightlineCode.replace(/\n+$/, "\n")}\n${emitBindNative(ctx)}\n`;
|
|
587
|
+
}
|
|
588
|
+
const structs = [];
|
|
589
|
+
for (const c of native) {
|
|
590
|
+
if (isNativeMapComponent(c))
|
|
591
|
+
structs.push(emitMapPortsStructs(c));
|
|
592
|
+
else
|
|
593
|
+
for (const n of c.body)
|
|
594
|
+
structs.push(emitPortsStruct(c, n));
|
|
595
|
+
}
|
|
596
|
+
const runners = native.map((c) => (isNativeMapComponent(c) ? emitNativeMapRunner(c) : emitNativeRunner(c))).join("\n\n");
|
|
597
|
+
const banner = `// ── NATIVE-PORTS layer (bc 0.5.0 — de-plumb the port container) ────────────────────
|
|
598
|
+
// A native-eligible component is emitted ONLY here (run_native_ + bind_native) — its generic
|
|
599
|
+
// key-value run_ / bind is NOT emitted (a fully-native module has zero generic port container:
|
|
600
|
+
// no vec! of (heap-key, boxed Value)). Each componentRef constructs a native ports struct by
|
|
601
|
+
// direct construction and dispatches via the native handler seam (NativeComponentExec).
|
|
602
|
+
// Components that are NOT native-eligible (map/cond/concurrency) keep the generic key-value
|
|
603
|
+
// run_ / bind above. Fail-closed operator semantics still route through the primitives.`;
|
|
604
|
+
const sections = [
|
|
605
|
+
banner,
|
|
606
|
+
`// Native ports structs (one per componentRef node; typed per the static port type).\n${structs.join("\n\n")}`,
|
|
607
|
+
`// Native-ports straight-line runners (called by bind_native).\n${runners}`,
|
|
608
|
+
emitBindNative(ctx),
|
|
609
|
+
];
|
|
610
|
+
return `${straightlineCode.replace(/\n+$/, "\n")}\n${sections.join("\n\n")}\n`;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* rustStraightlineNativeEmitter — language id `rust-straightline-native` (bc 0.5.0). Emits the
|
|
614
|
+
* generic straight-line body plus the NATIVE-PORTS layer (per-node ports struct + bind_native
|
|
615
|
+
* through NativeComponentExec). Additive: the generic bind / literal / ir endpoints are unchanged.
|
|
616
|
+
*/
|
|
617
|
+
export const rustStraightlineNativeEmitter = {
|
|
618
|
+
language: "rust-straightline-native",
|
|
619
|
+
fileExtension: ".rs",
|
|
620
|
+
defaultRuntimeImport: "behavior_contracts",
|
|
621
|
+
filenameHint: "behaviors_straightline_native_generated.rs",
|
|
622
|
+
emit,
|
|
623
|
+
};
|
|
624
|
+
//# sourceMappingURL=emit-straightline-native-rust.js.map
|