behavior-contracts 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/behavior.d.ts +7 -0
  2. package/dist/behavior.d.ts.map +1 -1
  3. package/dist/behavior.js.map +1 -1
  4. package/dist/builder.d.ts.map +1 -1
  5. package/dist/builder.js +10 -3
  6. package/dist/builder.js.map +1 -1
  7. package/dist/generator/async-plan.d.ts +78 -0
  8. package/dist/generator/async-plan.d.ts.map +1 -0
  9. package/dist/generator/async-plan.js +116 -0
  10. package/dist/generator/async-plan.js.map +1 -0
  11. package/dist/generator/core.d.ts +30 -9
  12. package/dist/generator/core.d.ts.map +1 -1
  13. package/dist/generator/core.js.map +1 -1
  14. package/dist/generator/{emit-straightline-typed-go.d.ts → emit-shared-go-typed.d.ts} +11 -17
  15. package/dist/generator/emit-shared-go-typed.d.ts.map +1 -0
  16. package/dist/generator/emit-shared-go-typed.js +572 -0
  17. package/dist/generator/emit-shared-go-typed.js.map +1 -0
  18. package/dist/generator/emit-shared-go.d.ts +29 -0
  19. package/dist/generator/emit-shared-go.d.ts.map +1 -0
  20. package/dist/generator/{emit-straightline-go.js → emit-shared-go.js} +2 -34
  21. package/dist/generator/emit-shared-go.js.map +1 -0
  22. package/dist/generator/{emit-straightline-typed-rust.d.ts → emit-shared-rust-typed.d.ts} +11 -17
  23. package/dist/generator/emit-shared-rust-typed.d.ts.map +1 -0
  24. package/dist/generator/emit-shared-rust-typed.js +576 -0
  25. package/dist/generator/emit-shared-rust-typed.js.map +1 -0
  26. package/dist/generator/{emit-straightline-rust.d.ts → emit-shared-rust.d.ts} +1 -36
  27. package/dist/generator/emit-shared-rust.d.ts.map +1 -0
  28. package/dist/generator/{emit-straightline-rust.js → emit-shared-rust.js} +2 -16
  29. package/dist/generator/emit-shared-rust.js.map +1 -0
  30. package/dist/generator/emit-shared-typescript.d.ts +12 -0
  31. package/dist/generator/emit-shared-typescript.d.ts.map +1 -0
  32. package/dist/generator/{emit-straightline-typescript.js → emit-shared-typescript.js} +5 -20
  33. package/dist/generator/emit-shared-typescript.js.map +1 -0
  34. package/dist/generator/emit-straightline-typed-typescript.d.ts +1 -1
  35. package/dist/generator/emit-straightline-typed-typescript.d.ts.map +1 -1
  36. package/dist/generator/emit-straightline-typed-typescript.js +1 -1
  37. package/dist/generator/emit-straightline-typed-typescript.js.map +1 -1
  38. package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
  39. package/dist/generator/emit-typed-native-go.js +495 -75
  40. package/dist/generator/emit-typed-native-go.js.map +1 -1
  41. package/dist/generator/emit-typed-native-rust.d.ts +2 -1
  42. package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
  43. package/dist/generator/emit-typed-native-rust.js +553 -103
  44. package/dist/generator/emit-typed-native-rust.js.map +1 -1
  45. package/dist/generator/index.d.ts +2 -12
  46. package/dist/generator/index.d.ts.map +1 -1
  47. package/dist/generator/index.js +10 -62
  48. package/dist/generator/index.js.map +1 -1
  49. package/dist/generator/native-expr.d.ts +120 -0
  50. package/dist/generator/native-expr.d.ts.map +1 -0
  51. package/dist/generator/native-expr.js +456 -0
  52. package/dist/generator/native-expr.js.map +1 -0
  53. package/dist/generator/typed.d.ts.map +1 -1
  54. package/dist/generator/typed.js +8 -0
  55. package/dist/generator/typed.js.map +1 -1
  56. package/dist/guard.js +14 -0
  57. package/dist/guard.js.map +1 -1
  58. package/dist/index.d.ts +2 -2
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +1 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/plan.d.ts +6 -2
  63. package/dist/plan.d.ts.map +1 -1
  64. package/dist/plan.js +4 -0
  65. package/dist/plan.js.map +1 -1
  66. package/package.json +1 -1
  67. package/dist/generator/emit-go.d.ts +0 -19
  68. package/dist/generator/emit-go.d.ts.map +0 -1
  69. package/dist/generator/emit-go.js +0 -79
  70. package/dist/generator/emit-go.js.map +0 -1
  71. package/dist/generator/emit-raw-abi-go.d.ts +0 -45
  72. package/dist/generator/emit-raw-abi-go.d.ts.map +0 -1
  73. package/dist/generator/emit-raw-abi-go.js +0 -346
  74. package/dist/generator/emit-raw-abi-go.js.map +0 -1
  75. package/dist/generator/emit-raw-abi-rust.d.ts +0 -45
  76. package/dist/generator/emit-raw-abi-rust.d.ts.map +0 -1
  77. package/dist/generator/emit-raw-abi-rust.js +0 -380
  78. package/dist/generator/emit-raw-abi-rust.js.map +0 -1
  79. package/dist/generator/emit-rust.d.ts +0 -25
  80. package/dist/generator/emit-rust.d.ts.map +0 -1
  81. package/dist/generator/emit-rust.js +0 -125
  82. package/dist/generator/emit-rust.js.map +0 -1
  83. package/dist/generator/emit-straightline-go.d.ts +0 -60
  84. package/dist/generator/emit-straightline-go.d.ts.map +0 -1
  85. package/dist/generator/emit-straightline-go.js.map +0 -1
  86. package/dist/generator/emit-straightline-native-go.d.ts +0 -54
  87. package/dist/generator/emit-straightline-native-go.d.ts.map +0 -1
  88. package/dist/generator/emit-straightline-native-go.js +0 -835
  89. package/dist/generator/emit-straightline-native-go.js.map +0 -1
  90. package/dist/generator/emit-straightline-native-rust.d.ts +0 -27
  91. package/dist/generator/emit-straightline-native-rust.d.ts.map +0 -1
  92. package/dist/generator/emit-straightline-native-rust.js +0 -624
  93. package/dist/generator/emit-straightline-native-rust.js.map +0 -1
  94. package/dist/generator/emit-straightline-php.d.ts +0 -46
  95. package/dist/generator/emit-straightline-php.d.ts.map +0 -1
  96. package/dist/generator/emit-straightline-php.js +0 -941
  97. package/dist/generator/emit-straightline-php.js.map +0 -1
  98. package/dist/generator/emit-straightline-python.d.ts +0 -36
  99. package/dist/generator/emit-straightline-python.d.ts.map +0 -1
  100. package/dist/generator/emit-straightline-python.js +0 -855
  101. package/dist/generator/emit-straightline-python.js.map +0 -1
  102. package/dist/generator/emit-straightline-rust.d.ts.map +0 -1
  103. package/dist/generator/emit-straightline-rust.js.map +0 -1
  104. package/dist/generator/emit-straightline-typed-go.d.ts.map +0 -1
  105. package/dist/generator/emit-straightline-typed-go.js +0 -1114
  106. package/dist/generator/emit-straightline-typed-go.js.map +0 -1
  107. package/dist/generator/emit-straightline-typed-rust.d.ts.map +0 -1
  108. package/dist/generator/emit-straightline-typed-rust.js +0 -1155
  109. package/dist/generator/emit-straightline-typed-rust.js.map +0 -1
  110. package/dist/generator/emit-straightline-typescript.d.ts +0 -47
  111. package/dist/generator/emit-straightline-typescript.d.ts.map +0 -1
  112. package/dist/generator/emit-straightline-typescript.js.map +0 -1
@@ -1,624 +0,0 @@
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