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,835 @@
|
|
|
1
|
+
import { goStraightlineEmitter, goStraightlineInternals } from "./emit-straightline-go.js";
|
|
2
|
+
import { buildComponentPlan, sequentialOrder, analyzeSequentialOps, classifyExpr, } from "./straightline.js";
|
|
3
|
+
const PKG = "dslcontracts";
|
|
4
|
+
const { sanitize, emitExpr } = goStraightlineInternals;
|
|
5
|
+
/** Go struct field name for a wire port name (exported, deterministic). */
|
|
6
|
+
function goPortFieldName(wire) {
|
|
7
|
+
const safe = wire.replace(/[^A-Za-z0-9_]/g, "_");
|
|
8
|
+
const head = safe.charAt(0);
|
|
9
|
+
return /[a-z]/.test(head) ? head.toUpperCase() + safe.slice(1) : "F_" + safe;
|
|
10
|
+
}
|
|
11
|
+
/** Native ports struct type name for a node (component + node id, module-unique). */
|
|
12
|
+
function portsStructName(compName, nodeId) {
|
|
13
|
+
return `ports_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
14
|
+
}
|
|
15
|
+
/** Local var name for an evaluated port value. */
|
|
16
|
+
function portLocal(nodeId, portName) {
|
|
17
|
+
return `pv_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}_${portName.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* inferPortType — derive the static Go field type of a port from its wired expression and the
|
|
21
|
+
* entry component's inputPorts (the only declared types available; the callee is a catalog
|
|
22
|
+
* reference, not in the IR). Conservative: only a shape we can PROVE the type of is typed;
|
|
23
|
+
* everything else is a dynamic `Value` field (still no map). This is the documented dynamic
|
|
24
|
+
* port fallback.
|
|
25
|
+
*/
|
|
26
|
+
function inferPortType(node, inputPorts) {
|
|
27
|
+
// A static arr port is a `[]dslcontracts.Value` — carried in a boxed `Value` struct field
|
|
28
|
+
// (still no map, no ArrNative closure slice; built by direct []Value{...} assignment).
|
|
29
|
+
if (staticArrElems(node) !== null)
|
|
30
|
+
return "Value";
|
|
31
|
+
const e = classifyExpr(node);
|
|
32
|
+
return inferFromStatic(e, inputPorts);
|
|
33
|
+
}
|
|
34
|
+
function inferFromStatic(e, inputPorts) {
|
|
35
|
+
switch (e.kind) {
|
|
36
|
+
case "str":
|
|
37
|
+
return "string";
|
|
38
|
+
case "bool":
|
|
39
|
+
return "bool";
|
|
40
|
+
case "concat":
|
|
41
|
+
// concat always yields a string (or fails TYPE_MISMATCH at runtime via the primitive).
|
|
42
|
+
return "string";
|
|
43
|
+
case "ref": {
|
|
44
|
+
// A single-segment ref to a declared input param carries that param's type.
|
|
45
|
+
if (e.path.length === 1) {
|
|
46
|
+
const schema = inputPorts[e.path[0]];
|
|
47
|
+
if (schema) {
|
|
48
|
+
switch (schema.type) {
|
|
49
|
+
case "string":
|
|
50
|
+
return "string";
|
|
51
|
+
case "int":
|
|
52
|
+
return "int64";
|
|
53
|
+
case "float":
|
|
54
|
+
return "float64";
|
|
55
|
+
case "bool":
|
|
56
|
+
return "bool";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return "Value";
|
|
61
|
+
}
|
|
62
|
+
case "null":
|
|
63
|
+
case "dynamic":
|
|
64
|
+
default:
|
|
65
|
+
return "Value";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A port is "infallible-literal" when it is a string/bool/null literal — it cannot produce an
|
|
70
|
+
* expression Failure, so it needs NO error check (no slVE wrapper, no dead branch). Everything
|
|
71
|
+
* else (ref, concat, dynamic operator) keeps a direct error return.
|
|
72
|
+
*/
|
|
73
|
+
function isInfallibleLiteral(node) {
|
|
74
|
+
const e = classifyExpr(node);
|
|
75
|
+
return e.kind === "str" || e.kind === "bool" || e.kind === "null";
|
|
76
|
+
}
|
|
77
|
+
/** Go literal for an infallible literal expression. */
|
|
78
|
+
function goLiteral(node) {
|
|
79
|
+
if (node === null)
|
|
80
|
+
return "nil";
|
|
81
|
+
if (typeof node === "boolean")
|
|
82
|
+
return node ? "true" : "false";
|
|
83
|
+
if (typeof node === "string")
|
|
84
|
+
return JSON.stringify(node);
|
|
85
|
+
return "nil";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A component is native-emittable when it is the clean read/write HOT PATH the owner cites:
|
|
89
|
+
* - strictly sequential (no RunPlan / real concurrency),
|
|
90
|
+
* - every body node is a plain componentRef (no map/cond),
|
|
91
|
+
* - no bindField (skip-on-null-binding needs the generic skip-propagation machinery),
|
|
92
|
+
* - default 'fail' policy on every node (continue/retry skip semantics stay on the generic path),
|
|
93
|
+
* - no relationKind (unproduced/connection shaping stays on the generic path),
|
|
94
|
+
* - every port expr and the output are STATICALLY resolvable (literal / ref / concat) — a
|
|
95
|
+
* dynamic operator (coalesce/cond/arith/obj/arr/…) in a port OR the output keeps the whole
|
|
96
|
+
* component on the generic path (its fail-closed semantics live in the primitive SSoT, and
|
|
97
|
+
* skip/unproduced interplay with those operators is subtle — we do NOT half-cover it).
|
|
98
|
+
* Anything else falls through to the generic straight-line Bind (correct, just not de-plumbed).
|
|
99
|
+
* This is fail-closed conservative: the native path only claims components it can de-plumb with
|
|
100
|
+
* PROVABLE run_behavior equivalence (the conformance gate compiles + compares every one).
|
|
101
|
+
*/
|
|
102
|
+
function isNativeComponent(comp) {
|
|
103
|
+
if (sequentialOrder(comp) === null)
|
|
104
|
+
return false;
|
|
105
|
+
for (const n of comp.body) {
|
|
106
|
+
if (!("component" in n) || "map" in n || "cond" in n)
|
|
107
|
+
return false;
|
|
108
|
+
const cr = n;
|
|
109
|
+
if (cr.bindField !== undefined)
|
|
110
|
+
return false;
|
|
111
|
+
if (cr.relationKind !== undefined)
|
|
112
|
+
return false;
|
|
113
|
+
if (cr.policy !== undefined && cr.policy !== "fail")
|
|
114
|
+
return false;
|
|
115
|
+
for (const p of Object.keys(cr.ports))
|
|
116
|
+
if (!isStaticallyResolvable(cr.ports[p]))
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return isStaticallyResolvable(comp.output);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* isNativeMapComponent — a component whose body is a SINGLE `map` node is native-eligible when:
|
|
123
|
+
* - the map's `over` is a ref (statically resolvable to an input param / prior node — here the
|
|
124
|
+
* single-node case means an input param),
|
|
125
|
+
* - every per-element port is statically resolvable (literal / ref-into-element / concat /
|
|
126
|
+
* static arr) — refs into the element binding (`as`) resolve to the element native local,
|
|
127
|
+
* - no `when` guard, no `into` zip-attach (those keep the generic path — element/kept
|
|
128
|
+
* alignment with guard/into interplay stays single-sourced),
|
|
129
|
+
* - default 'fail' policy, no relationKind,
|
|
130
|
+
* - the output is statically resolvable (a ref to the map node result).
|
|
131
|
+
* The batched vs per-element dispatch, MAP_OVER_NOT_ARRAY / MAP_BATCH_RESULT_MISMATCH, and the
|
|
132
|
+
* kept/collected alignment are reproduced EXACTLY by emitNativeMapRunner — only the per-element
|
|
133
|
+
* ports CONTAINER + dispatch go native. Everything else stays on the generic path.
|
|
134
|
+
*/
|
|
135
|
+
function isNativeMapComponent(comp) {
|
|
136
|
+
if (sequentialOrder(comp) === null)
|
|
137
|
+
return false;
|
|
138
|
+
if (comp.body.length !== 1)
|
|
139
|
+
return false;
|
|
140
|
+
const n = comp.body[0];
|
|
141
|
+
if (!("map" in n))
|
|
142
|
+
return false;
|
|
143
|
+
const m = n.map;
|
|
144
|
+
if (m.when !== undefined)
|
|
145
|
+
return false;
|
|
146
|
+
if (m.into !== undefined)
|
|
147
|
+
return false;
|
|
148
|
+
if (m.relationKind !== undefined)
|
|
149
|
+
return false;
|
|
150
|
+
if (m.policy !== undefined && m.policy !== "fail")
|
|
151
|
+
return false;
|
|
152
|
+
// `over` must be a plain ref (to an input param — single-node component has no prior node).
|
|
153
|
+
if (classifyExpr(m.over).kind !== "ref")
|
|
154
|
+
return false;
|
|
155
|
+
for (const p of Object.keys(m.ports))
|
|
156
|
+
if (!isStaticallyResolvable(m.ports[p]))
|
|
157
|
+
return false;
|
|
158
|
+
return isStaticallyResolvable(comp.output);
|
|
159
|
+
}
|
|
160
|
+
/** A native component (sequential componentRef OR single map-write). */
|
|
161
|
+
function isNative(comp) {
|
|
162
|
+
return isNativeComponent(comp) || isNativeMapComponent(comp);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* staticArrElems — if `node` is a single-key `{arr:[...]}` shape, return its element nodes;
|
|
166
|
+
* else null. classifyExpr keeps `arr` as `dynamic` (its fail-closed construction lives in the
|
|
167
|
+
* primitive SSoT), so we recognize the STATIC arr shape here (every element itself static) and
|
|
168
|
+
* build it by direct `[]Value{...}` assignment — NOT the generic ArrNative(closure-slice) path.
|
|
169
|
+
*/
|
|
170
|
+
function staticArrElems(node) {
|
|
171
|
+
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
172
|
+
return null;
|
|
173
|
+
const keys = Object.keys(node);
|
|
174
|
+
if (keys.length !== 1 || keys[0] !== "arr")
|
|
175
|
+
return null;
|
|
176
|
+
const arg = node.arr;
|
|
177
|
+
return Array.isArray(arg) ? arg : null;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A node is statically resolvable when it is a literal / ref / refOpt / concat (of the same),
|
|
181
|
+
* OR a static `arr` whose every element is itself statically resolvable (built by direct
|
|
182
|
+
* `[]dslcontracts.Value{...}` assignment, no ArrNative closure slice).
|
|
183
|
+
*/
|
|
184
|
+
function isStaticallyResolvable(node) {
|
|
185
|
+
const arr = staticArrElems(node);
|
|
186
|
+
if (arr !== null)
|
|
187
|
+
return arr.every(isStaticallyResolvable);
|
|
188
|
+
const e = classifyExpr(node);
|
|
189
|
+
switch (e.kind) {
|
|
190
|
+
case "str":
|
|
191
|
+
case "bool":
|
|
192
|
+
case "null":
|
|
193
|
+
case "ref":
|
|
194
|
+
return true;
|
|
195
|
+
case "concat":
|
|
196
|
+
return e.parts.every((p) => isStaticallyResolvable(reconstruct(p)));
|
|
197
|
+
case "dynamic":
|
|
198
|
+
default:
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** Emit the native ports struct + PortReader for one componentRef node. */
|
|
203
|
+
function emitPortsStruct(comp, node) {
|
|
204
|
+
const structName = portsStructName(comp.name, node.id);
|
|
205
|
+
const portNames = Object.keys(node.ports);
|
|
206
|
+
if (portNames.length === 0) {
|
|
207
|
+
return `// ${structName} — native ports for node '${node.id}' (component ${node.component}); no ports.\ntype ${structName} struct{}\n\nfunc (p ${structName}) Port(string) (${PKG}.Value, bool) { return nil, false }`;
|
|
208
|
+
}
|
|
209
|
+
const fieldTypes = portNames.map((p) => inferPortType(node.ports[p], comp.inputPorts));
|
|
210
|
+
const fieldNames = portNames.map(goPortFieldName);
|
|
211
|
+
const nameW = Math.max(...fieldNames.map((n) => n.length));
|
|
212
|
+
const typeW = Math.max(...fieldTypes.map((t) => (t === "Value" ? `${PKG}.Value`.length : t.length)));
|
|
213
|
+
const fields = portNames
|
|
214
|
+
.map((p, i) => {
|
|
215
|
+
const gt = fieldTypes[i] === "Value" ? `${PKG}.Value` : fieldTypes[i];
|
|
216
|
+
return `\t${fieldNames[i].padEnd(nameW)} ${gt.padEnd(typeW)} // ${JSON.stringify(p)}`;
|
|
217
|
+
})
|
|
218
|
+
.join("\n");
|
|
219
|
+
// PortReader: box ONE field on demand (uniform read for consumers that do not type-switch).
|
|
220
|
+
const cases = portNames
|
|
221
|
+
.map((p, i) => `\tcase ${JSON.stringify(p)}:\n\t\treturn ${fieldTypes[i] === "Value" ? "p." + fieldNames[i] : `${PKG}.Value(p.${fieldNames[i]})`}, true`)
|
|
222
|
+
.join("\n");
|
|
223
|
+
return `// ${structName} — NATIVE ports for node '${node.id}' (component ${node.component}). Typed fields per the
|
|
224
|
+
// static port type; built by direct field assignment (no map alloc, no per-port boxing).
|
|
225
|
+
type ${structName} struct {
|
|
226
|
+
${fields}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Port implements dslcontracts.PortReader (boxes ONE field on demand; the hot-path consumer
|
|
230
|
+
// type-switches to this concrete struct and reads the typed fields with zero boxing instead).
|
|
231
|
+
func (p ${structName}) Port(name string) (${PKG}.Value, bool) {
|
|
232
|
+
switch name {
|
|
233
|
+
${cases}
|
|
234
|
+
}
|
|
235
|
+
return nil, false
|
|
236
|
+
}`;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* emitNativeRunner — the native-ports straight-line exec for one native component. Ports are
|
|
240
|
+
* evaluated (fallible ones with a direct error return; literals inline), the native struct is
|
|
241
|
+
* built by direct assignment, and the handler is called via ExecNativeHandler. The result of
|
|
242
|
+
* each node is a RawValue (native-backed); ref reads between nodes read the accumulated native
|
|
243
|
+
* node results (bc#74 cross-node ref — resolved from the native result, not a dynamic scope).
|
|
244
|
+
*/
|
|
245
|
+
function emitNativeRunner(comp) {
|
|
246
|
+
const order = sequentialOrder(comp);
|
|
247
|
+
const metas = analyzeSequentialOps(buildComponentPlan(comp, goStraightlineInternals.dialect, "scope").opsLiteral, order);
|
|
248
|
+
const fn = sanitize(comp.name);
|
|
249
|
+
// Node results live in native locals (a RawValue per node). #74: a later node's port ref to
|
|
250
|
+
// an earlier node's field reads THIS local directly — the native analogue of the straight-
|
|
251
|
+
// line node-result scope, with NO dynamic *Obj scope object built at all (so the residual map
|
|
252
|
+
// alloc is gone too). A ref head resolves statically to nr_<id> (prior node) or an input.Get.
|
|
253
|
+
const nodeResult = (id) => `nr_${id.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
254
|
+
const idToPos = new Map();
|
|
255
|
+
order.forEach((opIdx, k) => idToPos.set(comp.body[opIdx].id, k));
|
|
256
|
+
// A ref head is a prior node if it names a body node executed before position `atPos`.
|
|
257
|
+
const priorNodeAt = (head, atPos) => {
|
|
258
|
+
const p = idToPos.get(head);
|
|
259
|
+
return p !== undefined && p < atPos;
|
|
260
|
+
};
|
|
261
|
+
const lines = [];
|
|
262
|
+
lines.push(`// run_native_${fn} — NATIVE-PORTS straight-line exec (bc 0.5.0): each componentRef builds a native`);
|
|
263
|
+
lines.push(`// ports struct by direct field assignment (no map container, no slVE wrapper) and dispatches via`);
|
|
264
|
+
lines.push(`// ExecNativeHandler. Node results are native locals; cross-node refs read them directly (#74) —`);
|
|
265
|
+
lines.push(`// no dynamic scope *Obj is ever built, so a single-op point read allocates only the escaping struct.`);
|
|
266
|
+
lines.push(`func run_native_${fn}(handlers ${PKG}.NativeComponentExec, input *${PKG}.Obj) (${PKG}.Value, error) {`);
|
|
267
|
+
// NOTE: no `input = NewObj()` nil-normalization — nvBindVE is nil-safe, so a native module
|
|
268
|
+
// allocates ZERO generic *Obj. (A pure-native module has NO NewObj/Set anywhere — grep gate.)
|
|
269
|
+
for (const k of order.keys()) {
|
|
270
|
+
const i = order[k];
|
|
271
|
+
const node = comp.body[i];
|
|
272
|
+
const meta = metas[i];
|
|
273
|
+
const structName = portsStructName(comp.name, node.id);
|
|
274
|
+
const portNames = Object.keys(node.ports);
|
|
275
|
+
lines.push(`\t// ── op '${node.id}' (${node.component}) ──`);
|
|
276
|
+
const inits = [];
|
|
277
|
+
for (const pn of portNames) {
|
|
278
|
+
const expr = node.ports[pn];
|
|
279
|
+
const fieldName = goPortFieldName(pn);
|
|
280
|
+
const ftype = inferPortType(expr, comp.inputPorts);
|
|
281
|
+
if (isInfallibleLiteral(expr)) {
|
|
282
|
+
inits.push(`${fieldName}: ${goLiteral(expr)}`);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const local = portLocal(node.id, pn);
|
|
286
|
+
const resolved = emitStaticRefValue(expr, k, priorNodeAt, nodeResult, lines, local);
|
|
287
|
+
if (resolved) {
|
|
288
|
+
// resolved is a Go expr producing (Value); errors already handled inline.
|
|
289
|
+
if (ftype === "Value") {
|
|
290
|
+
inits.push(`${fieldName}: ${resolved}`);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
lines.push(`\t${local}T, ${local}Ok := (${resolved}).(${ftype})`);
|
|
294
|
+
lines.push(`\tif !${local}Ok {`, `\t\treturn nil, ${PKG}.NewExprFailure("TYPE_MISMATCH", "port ${pn}: expected ${ftype}, got "+${PKG}.TypeName(${resolved}))`, `\t}`);
|
|
295
|
+
inits.push(`${fieldName}: ${local}T`);
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
// Genuinely dynamic (multi-arg operator etc.): fall back to the primitive SSoT with an
|
|
300
|
+
// ephemeral scope built ONLY for this expression (rare; keeps semantics single-sourced).
|
|
301
|
+
lines.push(`\t${local}Scope := ${PKG}.NewObj()`);
|
|
302
|
+
lines.push(`\tfor _, sk := range input.Keys {`, `\t\t${local}Scope.Set(sk, input.Vals[sk])`, `\t}`);
|
|
303
|
+
for (let pj = 0; pj < k; pj++) {
|
|
304
|
+
const priorId = comp.body[order[pj]].id;
|
|
305
|
+
lines.push(`\t${local}Scope.Set(${JSON.stringify(priorId)}, ${nodeResult(priorId)})`);
|
|
306
|
+
}
|
|
307
|
+
lines.push(`\t${local}, ${local}Err := ${emitExpr(expr, `${local}Scope`)}`);
|
|
308
|
+
lines.push(`\tif ${local}Err != nil {`, `\t\treturn nil, ${local}Err`, `\t}`);
|
|
309
|
+
if (ftype === "Value") {
|
|
310
|
+
inits.push(`${fieldName}: ${local}`);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
lines.push(`\t${local}T, ${local}Ok := ${local}.(${ftype})`);
|
|
314
|
+
lines.push(`\tif !${local}Ok {`, `\t\treturn nil, ${PKG}.NewExprFailure("TYPE_MISMATCH", "port ${pn}: expected ${ftype}, got "+${PKG}.TypeName(${local}))`, `\t}`);
|
|
315
|
+
inits.push(`${fieldName}: ${local}T`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
lines.push(`\tports_${sanitize(node.id)} := ${structName}{${inits.join(", ")}}`);
|
|
319
|
+
const oc = `o_${sanitize(node.id)}`;
|
|
320
|
+
lines.push(`\t${oc}, ${oc}Resolved := ${PKG}.ExecNativeHandler(handlers, ${JSON.stringify(node.id)}, ${JSON.stringify(node.component)}, ports_${sanitize(node.id)}, nil)`);
|
|
321
|
+
lines.push(`\tif !${oc}Resolved {`, `\t\treturn nil, ${PKG}.NewBehaviorFailure("UNKNOWN_COMPONENT", "component '${node.component}' has no handler (fail-closed)")`, `\t}`);
|
|
322
|
+
if (meta.policy === "continue") {
|
|
323
|
+
lines.push(`\tvar ${nodeResult(node.id)} ${PKG}.Value = nil`);
|
|
324
|
+
lines.push(`\tif !${oc}.IsError {`, `\t\t${nodeResult(node.id)} = ${oc}.Ok`, `\t}`);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
lines.push(`\tif ${oc}.IsError {`);
|
|
328
|
+
const policy = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
|
|
329
|
+
lines.push(`\t\treturn nil, ${PKG}.NewPlanFailure("OP_FAILED", "operation '${node.id}' failed under '${policy}: "+${oc}.Err)`);
|
|
330
|
+
lines.push(`\t}`);
|
|
331
|
+
lines.push(`\t${nodeResult(node.id)} := ${oc}.Ok`, `\t_ = ${nodeResult(node.id)}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// output: statically resolve the ref/concat to node-result locals / input; dynamic falls back.
|
|
335
|
+
const outResolved = emitStaticRefValue(comp.output, order.length, priorNodeAt, nodeResult, lines, "out");
|
|
336
|
+
if (outResolved) {
|
|
337
|
+
lines.push(`\treturn ${outResolved}, nil`);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
lines.push(`\toutScope := ${PKG}.NewObj()`);
|
|
341
|
+
lines.push(`\tfor _, sk := range input.Keys {`, `\t\toutScope.Set(sk, input.Vals[sk])`, `\t}`);
|
|
342
|
+
for (const opIdx of order) {
|
|
343
|
+
const id = comp.body[opIdx].id;
|
|
344
|
+
lines.push(`\toutScope.Set(${JSON.stringify(id)}, ${nodeResult(id)})`);
|
|
345
|
+
}
|
|
346
|
+
lines.push(`\toutV, outErr := ${emitExpr(comp.output, "outScope")}`);
|
|
347
|
+
lines.push(`\tif outErr != nil {`, `\t\treturn nil, outErr`, `\t}`);
|
|
348
|
+
lines.push(`\treturn outV, nil`);
|
|
349
|
+
}
|
|
350
|
+
lines.push(`}`);
|
|
351
|
+
return lines.join("\n");
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* emitMapStaticVE — the slVE-equivalent for a map element port: a ref whose head is the element
|
|
355
|
+
* binding (`as`) reads the element native local via nvOkVE(<elVar>); any other ref head reads an
|
|
356
|
+
* input param via nvBindVE(input, head). literal/concat/nested compose the same as emitStaticVE.
|
|
357
|
+
* Returns null only for a genuinely dynamic operator (guarded out by isNativeMapComponent).
|
|
358
|
+
*/
|
|
359
|
+
function emitMapStaticVE(node, asName, elVar) {
|
|
360
|
+
const e = classifyExpr(node);
|
|
361
|
+
switch (e.kind) {
|
|
362
|
+
case "str":
|
|
363
|
+
return `nvValVE(${JSON.stringify(e.value)})`;
|
|
364
|
+
case "bool":
|
|
365
|
+
return `nvValVE(${e.value ? "true" : "false"})`;
|
|
366
|
+
case "null":
|
|
367
|
+
return `nvValVE(nil)`;
|
|
368
|
+
case "ref": {
|
|
369
|
+
const head = e.path[0];
|
|
370
|
+
const headVE = head === asName ? `nvOkVE(${elVar})` : `nvBindVE(input, ${JSON.stringify(head)})`;
|
|
371
|
+
if (e.path.length === 1)
|
|
372
|
+
return headVE;
|
|
373
|
+
const segs = e.path.slice(1).map((p) => JSON.stringify(p)).join(", ");
|
|
374
|
+
return `nvFieldVE(${e.opt ? '"refOpt"' : '"ref"'}, ${headVE}, []string{${segs}})`;
|
|
375
|
+
}
|
|
376
|
+
case "concat": {
|
|
377
|
+
const parts = [];
|
|
378
|
+
for (const p of e.parts) {
|
|
379
|
+
const sub = emitMapStaticVE(reconstruct(p), asName, elVar);
|
|
380
|
+
if (sub === null)
|
|
381
|
+
return null;
|
|
382
|
+
parts.push(sub);
|
|
383
|
+
}
|
|
384
|
+
return `nvCatVE(${parts.join(", ")})`;
|
|
385
|
+
}
|
|
386
|
+
default:
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/** Resolve a map element port to a Go Value expr (emitting error-return lines). Handles static
|
|
391
|
+
* arr by direct []Value{...} (recursing element-wise), else via emitMapStaticVE + unwrap. */
|
|
392
|
+
function emitMapPortValue(node, asName, elVar, lines, ind, tmp) {
|
|
393
|
+
const arr = staticArrElems(node);
|
|
394
|
+
if (arr !== null) {
|
|
395
|
+
const elems = arr.map((el, j) => emitMapPortValue(el, asName, elVar, lines, ind, `${tmp}_a${j}`));
|
|
396
|
+
return `[]${PKG}.Value{${elems.join(", ")}}`;
|
|
397
|
+
}
|
|
398
|
+
const e = classifyExpr(node);
|
|
399
|
+
if (e.kind === "str" || e.kind === "bool" || e.kind === "null")
|
|
400
|
+
return goLiteral(node);
|
|
401
|
+
const ve = emitMapStaticVE(node, asName, elVar);
|
|
402
|
+
// ve is non-null (guarded); unwrap {Val,Err} with a direct error return.
|
|
403
|
+
const rv = `${tmp}_v`;
|
|
404
|
+
lines.push(`${ind}${rv} := ${ve}`);
|
|
405
|
+
lines.push(`${ind}if ${rv}.Err != nil {`, `${ind}\treturn nil, ${rv}.Err`, `${ind}}`);
|
|
406
|
+
return `${rv}.Val`;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* emitNativeMapRunner — the NATIVE-PORTS exec for a single-map-node write component. Reproduces
|
|
410
|
+
* emitSeqMap's control flow EXACTLY (over→[]Value assertion, MAP_OVER_NOT_ARRAY, per-element
|
|
411
|
+
* loop, batched vs per-element dispatch, kept/collected alignment, MAP_BATCH_RESULT_MISMATCH) —
|
|
412
|
+
* but each element's ports are built as a NATIVE ports STRUCT (direct field assignment, no
|
|
413
|
+
* NewObj/Set) and dispatched via ExecNativeHandler. The batched dispatch passes a native items
|
|
414
|
+
* batch struct whose "items" port carries the []Value of per-element ports structs.
|
|
415
|
+
*/
|
|
416
|
+
function emitNativeMapRunner(comp) {
|
|
417
|
+
const fn = sanitize(comp.name);
|
|
418
|
+
const m = comp.body[0].map;
|
|
419
|
+
const id = comp.body[0].id;
|
|
420
|
+
const s = sanitize(id);
|
|
421
|
+
const structName = portsStructName(comp.name, id);
|
|
422
|
+
const asName = m.as;
|
|
423
|
+
const batched = m.batched === true;
|
|
424
|
+
const elVar = `el_${s}`;
|
|
425
|
+
const overVar = `over_${s}`;
|
|
426
|
+
const keptVar = `kept_${s}`;
|
|
427
|
+
const collected = `collected_${s}`;
|
|
428
|
+
const items = `items_${s}`;
|
|
429
|
+
// over head: a ref to an input param (single-node component → no prior node).
|
|
430
|
+
const overE = classifyExpr(m.over);
|
|
431
|
+
const overHead = overE.kind === "ref" ? overE.path[0] : "";
|
|
432
|
+
const lines = [];
|
|
433
|
+
lines.push(`// run_native_${fn} — NATIVE-PORTS single-map-write exec (bc 0.5.0): reproduces the generic`);
|
|
434
|
+
lines.push(`// map control flow byte-for-byte, but each element's ports are a native struct (direct field`);
|
|
435
|
+
lines.push(`// assignment, no NewObj/Set) dispatched via ExecNativeHandler (${batched ? "batched" : "per-element"}).`);
|
|
436
|
+
lines.push(`func run_native_${fn}(handlers ${PKG}.NativeComponentExec, input *${PKG}.Obj) (${PKG}.Value, error) {`);
|
|
437
|
+
// over evaluation + array assertion (matches emitSeqMap).
|
|
438
|
+
lines.push(`\t${overVar}VE := nvBindVE(input, ${JSON.stringify(overHead)})`);
|
|
439
|
+
lines.push(`\tif ${overVar}VE.Err != nil {`, `\t\treturn nil, ${overVar}VE.Err`, `\t}`);
|
|
440
|
+
lines.push(`\t${overVar}, ${overVar}IsArr := ${overVar}VE.Val.([]${PKG}.Value)`);
|
|
441
|
+
lines.push(`\tif !${overVar}IsArr {`, `\t\treturn nil, ${PKG}.NewBehaviorFailure("MAP_OVER_NOT_ARRAY", "map '${id}': 'over' did not evaluate to an array")`, `\t}`);
|
|
442
|
+
lines.push(`\t${keptVar} := make([]int, 0, len(${overVar}))`);
|
|
443
|
+
const portNames = Object.keys(m.ports);
|
|
444
|
+
const fieldTypes = portNames.map((p) => inferPortType(m.ports[p], comp.inputPorts));
|
|
445
|
+
const buildPorts = (ind, portsVar) => {
|
|
446
|
+
const inits = [];
|
|
447
|
+
for (let pi = 0; pi < portNames.length; pi++) {
|
|
448
|
+
const pn = portNames[pi];
|
|
449
|
+
const fieldName = goPortFieldName(pn);
|
|
450
|
+
const ftype = fieldTypes[pi];
|
|
451
|
+
// Infallible literal: assign the Go literal DIRECTLY to the typed field (no type assertion,
|
|
452
|
+
// no error branch) — matches emitNativeRunner's isInfallibleLiteral fast path.
|
|
453
|
+
if (isInfallibleLiteral(m.ports[pn])) {
|
|
454
|
+
inits.push(`${fieldName}: ${goLiteral(m.ports[pn])}`);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const tmp = `pv_${s}_${pn.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
458
|
+
const val = emitMapPortValue(m.ports[pn], asName, elVar, lines, ind, tmp);
|
|
459
|
+
if (ftype === "Value") {
|
|
460
|
+
inits.push(`${fieldName}: ${val}`);
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
lines.push(`${ind}${tmp}T, ${tmp}Ok := (${val}).(${ftype})`);
|
|
464
|
+
lines.push(`${ind}if !${tmp}Ok {`, `${ind}\treturn nil, ${PKG}.NewExprFailure("TYPE_MISMATCH", "port ${pn}: expected ${ftype}, got "+${PKG}.TypeName(${val}))`, `${ind}}`);
|
|
465
|
+
inits.push(`${fieldName}: ${tmp}T`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
lines.push(`${ind}${portsVar} := ${structName}{${inits.join(", ")}}`);
|
|
469
|
+
};
|
|
470
|
+
if (batched) {
|
|
471
|
+
lines.push(`\t${items} := make([]${PKG}.Value, 0, len(${overVar}))`);
|
|
472
|
+
lines.push(`\tfor ${elVar}Idx, ${elVar} := range ${overVar} {`);
|
|
473
|
+
buildPorts(`\t\t`, `ports_${s}`);
|
|
474
|
+
lines.push(`\t\t${items} = append(${items}, ports_${s})`, `\t\t${keptVar} = append(${keptVar}, ${elVar}Idx)`, `\t}`);
|
|
475
|
+
lines.push(`\tvar ${collected} []${PKG}.Value`);
|
|
476
|
+
lines.push(`\tif len(${items}) == 0 {`, `\t\t${collected} = []${PKG}.Value{}`, `\t} else {`);
|
|
477
|
+
// batched native ports: a struct exposing the "items" []Value (per-element native ports structs).
|
|
478
|
+
const batchStruct = `${structName}_batch`;
|
|
479
|
+
lines.push(`\t\tbp_${s} := ${batchStruct}{Items: ${items}}`);
|
|
480
|
+
const oc = `mo_${s}`;
|
|
481
|
+
lines.push(`\t\t${oc}, ${oc}Resolved := ${PKG}.ExecNativeHandler(handlers, ${JSON.stringify(id)}, ${JSON.stringify(m.component)}, bp_${s}, nil)`);
|
|
482
|
+
lines.push(`\t\tif !${oc}Resolved {`, `\t\t\treturn nil, ${PKG}.NewBehaviorFailure("UNKNOWN_COMPONENT", "component '${m.component}' has no handler (fail-closed)")`, `\t\t}`);
|
|
483
|
+
lines.push(`\t\tif ${oc}.IsError {`);
|
|
484
|
+
lines.push(`\t\t\treturn nil, ${PKG}.NewPlanFailure("OP_FAILED", "operation '${id}' failed under 'fail' policy: "+${oc}.Err)`);
|
|
485
|
+
lines.push(`\t\t}`);
|
|
486
|
+
lines.push(`\t\tr_batch_${s}, isArr_${s} := ${oc}.Ok.([]${PKG}.Value)`);
|
|
487
|
+
lines.push(`\t\tif !isArr_${s} || len(r_batch_${s}) != len(${items}) {`, `\t\t\treturn nil, ${PKG}.NewBehaviorFailure("MAP_BATCH_RESULT_MISMATCH", fmt.Sprintf("map '${id}': batched handler must return a list aligned to items (want %d)", len(${items})))`, `\t\t}`);
|
|
488
|
+
lines.push(`\t\t${collected} = r_batch_${s}`);
|
|
489
|
+
lines.push(`\t}`);
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
lines.push(`\t${collected} := make([]${PKG}.Value, 0, len(${overVar}))`);
|
|
493
|
+
lines.push(`\tfor ${elVar}Idx, ${elVar} := range ${overVar} {`);
|
|
494
|
+
buildPorts(`\t\t`, `ports_${s}`);
|
|
495
|
+
const oc = `mo_${s}`;
|
|
496
|
+
lines.push(`\t\t${oc}, ${oc}Resolved := ${PKG}.ExecNativeHandler(handlers, ${JSON.stringify(id)}, ${JSON.stringify(m.component)}, ports_${s}, ${elVar})`);
|
|
497
|
+
lines.push(`\t\tif !${oc}Resolved {`, `\t\t\treturn nil, ${PKG}.NewBehaviorFailure("UNKNOWN_COMPONENT", "component '${m.component}' has no handler (fail-closed)")`, `\t\t}`);
|
|
498
|
+
lines.push(`\t\tif ${oc}.IsError {`);
|
|
499
|
+
lines.push(`\t\t\treturn nil, ${PKG}.NewPlanFailure("OP_FAILED", "operation '${id}' failed under 'fail' policy: "+${oc}.Err)`);
|
|
500
|
+
lines.push(`\t\t}`);
|
|
501
|
+
lines.push(`\t\t${collected} = append(${collected}, ${oc}.Ok)`, `\t\t${keptVar} = append(${keptVar}, ${elVar}Idx)`, `\t}`);
|
|
502
|
+
}
|
|
503
|
+
// node result = collected (no `into`). output resolves ref/arr/concat over this node result.
|
|
504
|
+
lines.push(`\tnr_${s} := ${PKG}.Value(${collected})`);
|
|
505
|
+
lines.push(`\t_ = ${keptVar}`);
|
|
506
|
+
lines.push(`\t_ = nr_${s}`);
|
|
507
|
+
// output: a ref to the map node result (or a static arr/concat wrapping it). emitStaticRefValue
|
|
508
|
+
// resolves the map node's id to nr_<s> and handles the static-arr output wrapper directly.
|
|
509
|
+
const priorNodeAt = (head) => head === id;
|
|
510
|
+
const nodeResult = () => `nr_${s}`;
|
|
511
|
+
const outResolved = emitStaticRefValue(comp.output, 1, (h, _p) => priorNodeAt(h), nodeResult, lines, "out");
|
|
512
|
+
if (outResolved) {
|
|
513
|
+
lines.push(`\treturn ${outResolved}, nil`);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
lines.push(`\treturn nil, ${PKG}.NewBehaviorFailure("UNSUPPORTED_OUTPUT_NATIVE", "map component '${comp.name}': output not statically resolvable")`);
|
|
517
|
+
}
|
|
518
|
+
lines.push(`}`);
|
|
519
|
+
return lines.join("\n");
|
|
520
|
+
}
|
|
521
|
+
/** Emit the per-node native ports struct(s) for a single-map-write component: the element ports
|
|
522
|
+
* struct + a batched "items" struct (when batched) exposing the []Value items via PortReader. */
|
|
523
|
+
function emitMapPortsStructs(comp) {
|
|
524
|
+
const m = comp.body[0].map;
|
|
525
|
+
const id = comp.body[0].id;
|
|
526
|
+
const structName = portsStructName(comp.name, id);
|
|
527
|
+
const elemStruct = emitPortsStruct(comp, { id, component: m.component, ports: m.ports });
|
|
528
|
+
if (m.batched !== true)
|
|
529
|
+
return elemStruct;
|
|
530
|
+
const batchStruct = `${structName}_batch`;
|
|
531
|
+
const portNames = Object.keys(m.ports);
|
|
532
|
+
// Port("items") reconstruction: a consumer on the hot path DOWNCASTS to []<${structName}> and
|
|
533
|
+
// reads typed element fields with zero boxing (no map alloc — the whole de-plumb). This uniform
|
|
534
|
+
// PortReader path is a FALLBACK for a consumer that reads by name; it materializes the generic
|
|
535
|
+
// {items:[{...}]} view ONLY on demand (never touched by the de-plumbed run_native_ runner, which
|
|
536
|
+
// passes the native []${structName} straight through — the runner stays NewObj/Set-free).
|
|
537
|
+
const reconstruct = `\t\t\to := ${PKG}.NewObj()\n\t\t\tpr, _ := e.(${PKG}.PortReader)\n\t\t\t_ = pr\n${portNames
|
|
538
|
+
.map((pn) => `\t\t\tif pr != nil {\n\t\t\t\tif v, has := pr.Port(${JSON.stringify(pn)}); has {\n\t\t\t\t\to.Set(${JSON.stringify(pn)}, v)\n\t\t\t\t}\n\t\t\t}`)
|
|
539
|
+
.join("\n")}\n\t\t\tout[i] = o`;
|
|
540
|
+
return `${elemStruct}
|
|
541
|
+
|
|
542
|
+
// ${batchStruct} — NATIVE batched ports for map '${id}': Items holds the typed per-element native
|
|
543
|
+
// ports structs (a []Value of concrete ${structName}, built by direct assignment; no key-value
|
|
544
|
+
// container in the RUNNER). A hot-path consumer downcasts each element to ${structName}; the
|
|
545
|
+
// uniform Port("items") accessor below reconstructs the generic {items:[{...}]} view on demand.
|
|
546
|
+
type ${batchStruct} struct {
|
|
547
|
+
Items []${PKG}.Value
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
func (p ${batchStruct}) Port(name string) (${PKG}.Value, bool) {
|
|
551
|
+
if name == "items" {
|
|
552
|
+
out := make([]${PKG}.Value, len(p.Items))
|
|
553
|
+
for i, e := range p.Items {
|
|
554
|
+
${reconstruct}
|
|
555
|
+
}
|
|
556
|
+
return out, true
|
|
557
|
+
}
|
|
558
|
+
return nil, false
|
|
559
|
+
}`;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* emitStaticRefValue — statically resolve a ref/refOpt/concat/literal expression to a Go
|
|
563
|
+
* expression that reads node-result locals (nr_<id>) or input params DIRECTLY, with no dynamic
|
|
564
|
+
* scope object. Returns the Go expression string (already emitting any needed field-walk +
|
|
565
|
+
* error-return lines), or null if the expression is a genuinely dynamic operator that must go
|
|
566
|
+
* through the primitive SSoT (the caller falls back to an ephemeral scope for those).
|
|
567
|
+
*
|
|
568
|
+
* This is the bc#74 native cross-node resolution: a ref head that names a prior node reads that
|
|
569
|
+
* node's NATIVE result value; a single-segment head is returned directly; a multi-segment path
|
|
570
|
+
* is walked with slFieldVE-equivalent semantics emitted inline via slRef over a 1-key ephemeral
|
|
571
|
+
* carrier ONLY when the head is a node result (kept as a direct value walk).
|
|
572
|
+
*/
|
|
573
|
+
function emitStaticRefValue(node, atPos, priorNodeAt, nodeResult, lines, tmpBase) {
|
|
574
|
+
const arr = staticArrElems(node);
|
|
575
|
+
if (arr !== null) {
|
|
576
|
+
// Static arr → direct []dslcontracts.Value{...} (NOT ArrNative closures). Each element is
|
|
577
|
+
// resolved via this same function (a literal inlines; a ref/concat emits its error-return
|
|
578
|
+
// lines and yields a Go Value expr). The whole arr is infallible once elements resolve.
|
|
579
|
+
const elems = arr.map((el, j) => emitStaticRefValue(el, atPos, priorNodeAt, nodeResult, lines, `${tmpBase}_a${j}`));
|
|
580
|
+
return `[]${PKG}.Value{${elems.join(", ")}}`;
|
|
581
|
+
}
|
|
582
|
+
const e = classifyExpr(node);
|
|
583
|
+
if (e.kind === "str" || e.kind === "bool" || e.kind === "null") {
|
|
584
|
+
return goLiteral(node);
|
|
585
|
+
}
|
|
586
|
+
// ref / concat reuse the native layer's own SSoT helpers (nvOkVE / nvBindVE / nvFieldVE /
|
|
587
|
+
// nvCatVE — emitted unconditionally in this layer, mirroring the straight-line slVE semantics
|
|
588
|
+
// so NULL_REF/MISSING_PROP/TYPE_MISMATCH/UNKNOWN_BINDING stay byte-identical to expr.go). They
|
|
589
|
+
// are self-contained (do NOT depend on the generic body's usage-flagged helpers, which may be
|
|
590
|
+
// absent for a component whose generic path never used them). We produce an nvVE and unwrap
|
|
591
|
+
// {Val,Err} with a direct error return (the wrapper is folded away at the port).
|
|
592
|
+
const ve = emitStaticVE(node, atPos, priorNodeAt, nodeResult);
|
|
593
|
+
if (ve === null)
|
|
594
|
+
return null;
|
|
595
|
+
const rv = `${tmpBase}_v`;
|
|
596
|
+
lines.push(`\t${rv} := ${ve}`);
|
|
597
|
+
lines.push(`\tif ${rv}.Err != nil {`, `\t\treturn nil, ${rv}.Err`, `\t}`);
|
|
598
|
+
return `${rv}.Val`;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* emitStaticVE — build the slVE expression for a static ref/concat/literal, resolving ref heads
|
|
602
|
+
* to prior node-result locals (slOkVE(nr_<id>)) or input binds (slBindVE(input, head)). Returns
|
|
603
|
+
* null for a genuinely dynamic operator (caller falls back to the primitive SSoT).
|
|
604
|
+
*/
|
|
605
|
+
function emitStaticVE(node, atPos, priorNodeAt, nodeResult) {
|
|
606
|
+
const e = classifyExpr(node);
|
|
607
|
+
switch (e.kind) {
|
|
608
|
+
case "str":
|
|
609
|
+
return `nvValVE(${JSON.stringify(e.value)})`;
|
|
610
|
+
case "bool":
|
|
611
|
+
return `nvValVE(${e.value ? "true" : "false"})`;
|
|
612
|
+
case "null":
|
|
613
|
+
return `nvValVE(nil)`;
|
|
614
|
+
case "ref": {
|
|
615
|
+
const head = e.path[0];
|
|
616
|
+
const headVE = priorNodeAt(head, atPos)
|
|
617
|
+
? `nvOkVE(${nodeResult(head)})`
|
|
618
|
+
: `nvBindVE(input, ${JSON.stringify(head)})`;
|
|
619
|
+
if (e.path.length === 1)
|
|
620
|
+
return headVE;
|
|
621
|
+
const segs = e.path.slice(1).map((p) => JSON.stringify(p)).join(", ");
|
|
622
|
+
return `nvFieldVE(${e.opt ? '"refOpt"' : '"ref"'}, ${headVE}, []string{${segs}})`;
|
|
623
|
+
}
|
|
624
|
+
case "concat": {
|
|
625
|
+
const parts = [];
|
|
626
|
+
for (const p of e.parts) {
|
|
627
|
+
const sub = emitStaticVE(reconstruct(p), atPos, priorNodeAt, nodeResult);
|
|
628
|
+
if (sub === null)
|
|
629
|
+
return null;
|
|
630
|
+
parts.push(sub);
|
|
631
|
+
}
|
|
632
|
+
return `nvCatVE(${parts.join(", ")})`;
|
|
633
|
+
}
|
|
634
|
+
default:
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/** Self-contained native-layer VE helpers (mirror the straight-line slVE SSoT; expr.go codes). */
|
|
639
|
+
function emitNativeHelpers(needsCat) {
|
|
640
|
+
const base = `// nvVE packs a single (Value, error) so a statically-wired native port expr composes as one
|
|
641
|
+
// expression, then is folded away (the {Val,Err} never persists past the port). Mirrors slVE.
|
|
642
|
+
type nvVE struct {
|
|
643
|
+
Val ${PKG}.Value
|
|
644
|
+
Err error
|
|
645
|
+
}
|
|
646
|
+
func nvValVE(v ${PKG}.Value) nvVE { return nvVE{Val: v} }
|
|
647
|
+
func nvOkVE(v ${PKG}.Value) nvVE { return nvVE{Val: v} }
|
|
648
|
+
// nvBindVE reads an input-param head directly (UNKNOWN_BINDING matches expr.go). A nil scope
|
|
649
|
+
// (no input passed) has no bindings — fail closed with UNKNOWN_BINDING (no *Obj allocated).
|
|
650
|
+
func nvBindVE(scope *${PKG}.Obj, head string) nvVE {
|
|
651
|
+
if scope == nil {
|
|
652
|
+
return nvVE{Err: ${PKG}.NewExprFailure("UNKNOWN_BINDING", "unknown binding: "+head)}
|
|
653
|
+
}
|
|
654
|
+
v, ok := scope.Get(head)
|
|
655
|
+
if !ok {
|
|
656
|
+
return nvVE{Err: ${PKG}.NewExprFailure("UNKNOWN_BINDING", "unknown binding: "+head)}
|
|
657
|
+
}
|
|
658
|
+
return nvVE{Val: v}
|
|
659
|
+
}
|
|
660
|
+
// nvFieldVE walks a static field path over a resolved head (expr.go ref/refOpt semantics).
|
|
661
|
+
func nvFieldVE(op string, head nvVE, path []string) nvVE {
|
|
662
|
+
if head.Err != nil {
|
|
663
|
+
return head
|
|
664
|
+
}
|
|
665
|
+
cur := head.Val
|
|
666
|
+
for _, seg := range path {
|
|
667
|
+
if cur == nil {
|
|
668
|
+
if op == "refOpt" {
|
|
669
|
+
return nvVE{Val: nil}
|
|
670
|
+
}
|
|
671
|
+
return nvVE{Err: ${PKG}.NewExprFailure("NULL_REF", "null intermediate at ."+seg+" (use ?.)")}
|
|
672
|
+
}
|
|
673
|
+
o, ok := cur.(*${PKG}.Obj)
|
|
674
|
+
if !ok {
|
|
675
|
+
return nvVE{Err: ${PKG}.NewExprFailure("TYPE_MISMATCH", "cannot access ."+seg+" on "+${PKG}.TypeName(cur))}
|
|
676
|
+
}
|
|
677
|
+
next, present := o.Get(seg)
|
|
678
|
+
if !present {
|
|
679
|
+
return nvVE{Err: ${PKG}.NewExprFailure("MISSING_PROP", "missing property ."+seg)}
|
|
680
|
+
}
|
|
681
|
+
cur = next
|
|
682
|
+
}
|
|
683
|
+
return nvVE{Val: cur}
|
|
684
|
+
}`;
|
|
685
|
+
if (!needsCat)
|
|
686
|
+
return base;
|
|
687
|
+
return `${base}
|
|
688
|
+
// nvCatVE concatenates already-evaluated string parts (expr.go concat: strings only order + code).
|
|
689
|
+
func nvCatVE(parts ...nvVE) nvVE {
|
|
690
|
+
var b strings.Builder
|
|
691
|
+
for _, p := range parts {
|
|
692
|
+
if p.Err != nil {
|
|
693
|
+
return p
|
|
694
|
+
}
|
|
695
|
+
s, ok := p.Val.(string)
|
|
696
|
+
if !ok {
|
|
697
|
+
return nvVE{Err: ${PKG}.NewExprFailure("TYPE_MISMATCH", "concat: strings only (got "+${PKG}.TypeName(p.Val)+"; no implicit toString)")}
|
|
698
|
+
}
|
|
699
|
+
b.WriteString(s)
|
|
700
|
+
}
|
|
701
|
+
return nvVE{Val: b.String()}
|
|
702
|
+
}`;
|
|
703
|
+
}
|
|
704
|
+
/** Whether the native layer needs "strings" (a concat appears in any native component). */
|
|
705
|
+
function nativeNeedsStrings(comp) {
|
|
706
|
+
const walk = (x) => {
|
|
707
|
+
if (x === null || typeof x !== "object")
|
|
708
|
+
return false;
|
|
709
|
+
if (Array.isArray(x))
|
|
710
|
+
return x.some(walk);
|
|
711
|
+
const keys = Object.keys(x);
|
|
712
|
+
if (keys.length === 1 && keys[0] === "concat")
|
|
713
|
+
return true;
|
|
714
|
+
return keys.some((k) => walk(x[k]));
|
|
715
|
+
};
|
|
716
|
+
return comp.body.some((n) => walk(n)) || walk(comp.output);
|
|
717
|
+
}
|
|
718
|
+
/** Reconstruct an Expression IR node from a classified StaticExpr (for recursive concat parts). */
|
|
719
|
+
function reconstruct(e) {
|
|
720
|
+
switch (e.kind) {
|
|
721
|
+
case "str":
|
|
722
|
+
return e.value;
|
|
723
|
+
case "bool":
|
|
724
|
+
return e.value;
|
|
725
|
+
case "null":
|
|
726
|
+
return null;
|
|
727
|
+
case "ref":
|
|
728
|
+
return { [e.opt ? "refOpt" : "ref"]: e.path };
|
|
729
|
+
case "concat":
|
|
730
|
+
return { concat: e.parts.map(reconstruct) };
|
|
731
|
+
case "dynamic":
|
|
732
|
+
return e.node;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/** BindNative — dispatch surface for native components (mirrors Bind, native handler seam). */
|
|
736
|
+
function emitBindNative(ctx) {
|
|
737
|
+
const native = ctx.ir.components.filter(isNative);
|
|
738
|
+
const arms = native
|
|
739
|
+
.map((c) => `\tm[${JSON.stringify(c.name)}] = func(input *${PKG}.Obj) (${PKG}.Value, error) {\n\t\treturn run_native_${sanitize(c.name)}(handlers, input)\n\t}`)
|
|
740
|
+
.join("\n");
|
|
741
|
+
return `// BindNative is the NATIVE-PORTS dispatch surface (bc 0.5.0): it binds a NativeComponentExec
|
|
742
|
+
// (native ports struct + native-backed result) to the de-plumbed runners. Only native-covered
|
|
743
|
+
// components (strictly-sequential componentRef) are exposed; a consumer keeps the rest on the
|
|
744
|
+
// generic Bind. This is ADDITIVE — the generic Bind path is unchanged.
|
|
745
|
+
func BindNative(handlers ${PKG}.NativeComponentExec) map[string]func(input *${PKG}.Obj) (${PKG}.Value, error) {
|
|
746
|
+
m := map[string]func(input *${PKG}.Obj) (${PKG}.Value, error){}
|
|
747
|
+
${arms}
|
|
748
|
+
return m
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// ComponentNamesNative — components exposed on the native-ports path (in declaration order).
|
|
752
|
+
var ComponentNamesNative = []string{${native.map((c) => JSON.stringify(c.name)).join(", ")}}`;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* emit — a native-eligible component gets ONLY the native run_native_ / BindNative path; the
|
|
756
|
+
* generic key-value run_ / Bind is emitted ONLY for components that are NOT native-eligible
|
|
757
|
+
* (map/cond/concurrency), so a fully-native module contains ZERO NewObj/Set/generic ExecHandler
|
|
758
|
+
* (owner purity requirement + grep gate). We drive the generic emitter with a FILTERED context
|
|
759
|
+
* (non-native components only) so it emits the generic body — imports, spec-version init, base
|
|
760
|
+
* helpers, run_ fns, Bind, dispatch, ComponentNames — for exactly those components. The native
|
|
761
|
+
* surface (structs, run_native_ fns, BindNative, ComponentNamesNative) covers the native ones.
|
|
762
|
+
*/
|
|
763
|
+
function emit(ctx) {
|
|
764
|
+
const native = ctx.ir.components.filter(isNative);
|
|
765
|
+
const nonNative = ctx.ir.components.filter((c) => !isNative(c));
|
|
766
|
+
// Generic body for the NON-NATIVE components only (filtered context). When every component is
|
|
767
|
+
// native, this body has zero components: header + spec-version init + slVal base helper +
|
|
768
|
+
// empty Bind/dispatch/ComponentNames — and crucially NO NewObj/Set/ExecHandler (those live in
|
|
769
|
+
// run_*, which only exist for the non-native components here).
|
|
770
|
+
const filteredCtx = {
|
|
771
|
+
...ctx,
|
|
772
|
+
ir: { ...ctx.ir, components: nonNative },
|
|
773
|
+
componentNames: nonNative.map((c) => c.name),
|
|
774
|
+
};
|
|
775
|
+
let straightlineCode = goStraightlineEmitter.emit(filteredCtx);
|
|
776
|
+
if (native.length === 0) {
|
|
777
|
+
// Nothing native-emittable (all map/cond/concurrency): generic path only. BindNative binds
|
|
778
|
+
// nothing. The generic body is the FULL module (nonNative === all components).
|
|
779
|
+
return `${straightlineCode.replace(/\n+$/, "\n")}\n${emitBindNative(ctx)}\n`;
|
|
780
|
+
}
|
|
781
|
+
const structs = [];
|
|
782
|
+
for (const c of native) {
|
|
783
|
+
if (isNativeMapComponent(c))
|
|
784
|
+
structs.push(emitMapPortsStructs(c));
|
|
785
|
+
else
|
|
786
|
+
for (const n of c.body)
|
|
787
|
+
structs.push(emitPortsStruct(c, n));
|
|
788
|
+
}
|
|
789
|
+
const runners = native.map((c) => (isNativeMapComponent(c) ? emitNativeMapRunner(c) : emitNativeRunner(c))).join("\n\n");
|
|
790
|
+
const bindNative = emitBindNative(ctx);
|
|
791
|
+
const needsCat = native.some(nativeNeedsStrings);
|
|
792
|
+
const helpers = emitNativeHelpers(needsCat);
|
|
793
|
+
// A batched native map runner uses fmt.Sprintf (MAP_BATCH_RESULT_MISMATCH). Ensure "fmt" is
|
|
794
|
+
// imported even if no NON-NATIVE component needed it (idempotent: skip if present).
|
|
795
|
+
const needsFmt = native.some((c) => isNativeMapComponent(c) && c.body[0].map.batched === true);
|
|
796
|
+
if (needsFmt && !/\n\t"fmt"\n/.test(straightlineCode)) {
|
|
797
|
+
// inject after the opening import "(" — the generic header always emits an import block.
|
|
798
|
+
straightlineCode = straightlineCode.replace(/\nimport \(\n/, '\nimport (\n\t"fmt"\n');
|
|
799
|
+
}
|
|
800
|
+
// nvCatVE uses strings.Builder. The filtered generic body imports "strings" iff a NON-NATIVE
|
|
801
|
+
// component uses concat; if concat appears only in native components (or the module is fully
|
|
802
|
+
// native), inject the "strings" import so nvCatVE resolves. (Idempotent: skip if present.)
|
|
803
|
+
if (needsCat && !/\n\t"strings"\n/.test(straightlineCode)) {
|
|
804
|
+
straightlineCode = straightlineCode.replace(/\n\t"fmt"\n/, '\n\t"fmt"\n\t"strings"\n');
|
|
805
|
+
}
|
|
806
|
+
const banner = `// ── NATIVE-PORTS layer (bc 0.5.0 — de-plumb the port container) ────────────────────
|
|
807
|
+
// A native-eligible component is emitted ONLY here (run_native_ + BindNative) — its generic
|
|
808
|
+
// key-value run_ / Bind is NOT emitted (a fully-native module has zero generic port container).
|
|
809
|
+
// Each componentRef builds a native ports struct by direct field assignment and dispatches via
|
|
810
|
+
// the native handler seam (NativeComponentExec -> ExecOutcome). Components that are NOT native-
|
|
811
|
+
// eligible (map/cond/concurrency) keep the generic key-value run_ / Bind above (they can't be
|
|
812
|
+
// de-plumbed yet). Fail-closed operator semantics still route through the A0 primitives.`;
|
|
813
|
+
const sections = [
|
|
814
|
+
banner,
|
|
815
|
+
`// Self-contained native-layer VE helpers (expr.go ref/concat semantics; independent of the\n// generic body's usage-flagged helpers).\n${helpers}`,
|
|
816
|
+
`// Native ports structs (one per componentRef node; typed per the static port type).\n${structs.join("\n\n")}`,
|
|
817
|
+
`// Native-ports straight-line runners: the de-plumbed exec path (called by BindNative).\n${runners}`,
|
|
818
|
+
bindNative,
|
|
819
|
+
];
|
|
820
|
+
return `${straightlineCode.replace(/\n+$/, "\n")}\n${sections.join("\n\n")}\n`;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* goStraightlineNativeEmitter — language id `go-straightline-native` (bc 0.5.0). Emits the
|
|
824
|
+
* generic straight-line body (fallback + helpers) plus the NATIVE-PORTS layer (per-node ports
|
|
825
|
+
* struct built by direct assignment + BindNative through the native handler seam). Additive:
|
|
826
|
+
* the generic Bind / literal / ir endpoints are unchanged.
|
|
827
|
+
*/
|
|
828
|
+
export const goStraightlineNativeEmitter = {
|
|
829
|
+
language: "go-straightline-native",
|
|
830
|
+
fileExtension: ".go",
|
|
831
|
+
defaultRuntimeImport: "github.com/foo-ogawa/behavior-contracts/go/coderuntime",
|
|
832
|
+
filenameHint: "behaviors.straightline.native.generated.go",
|
|
833
|
+
emit,
|
|
834
|
+
};
|
|
835
|
+
//# sourceMappingURL=emit-straightline-native-go.js.map
|