behavior-contracts 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/behavior.d.ts +7 -0
- package/dist/behavior.d.ts.map +1 -1
- package/dist/behavior.js.map +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +10 -3
- package/dist/builder.js.map +1 -1
- package/dist/generator/async-plan.d.ts +78 -0
- package/dist/generator/async-plan.d.ts.map +1 -0
- package/dist/generator/async-plan.js +116 -0
- package/dist/generator/async-plan.js.map +1 -0
- package/dist/generator/core.d.ts +30 -9
- package/dist/generator/core.d.ts.map +1 -1
- package/dist/generator/core.js.map +1 -1
- package/dist/generator/{emit-straightline-typed-go.d.ts → emit-shared-go-typed.d.ts} +11 -17
- package/dist/generator/emit-shared-go-typed.d.ts.map +1 -0
- package/dist/generator/emit-shared-go-typed.js +572 -0
- package/dist/generator/emit-shared-go-typed.js.map +1 -0
- package/dist/generator/emit-shared-go.d.ts +29 -0
- package/dist/generator/emit-shared-go.d.ts.map +1 -0
- package/dist/generator/{emit-straightline-go.js → emit-shared-go.js} +2 -34
- package/dist/generator/emit-shared-go.js.map +1 -0
- package/dist/generator/{emit-straightline-typed-rust.d.ts → emit-shared-rust-typed.d.ts} +11 -17
- package/dist/generator/emit-shared-rust-typed.d.ts.map +1 -0
- package/dist/generator/emit-shared-rust-typed.js +576 -0
- package/dist/generator/emit-shared-rust-typed.js.map +1 -0
- package/dist/generator/{emit-straightline-rust.d.ts → emit-shared-rust.d.ts} +1 -36
- package/dist/generator/emit-shared-rust.d.ts.map +1 -0
- package/dist/generator/{emit-straightline-rust.js → emit-shared-rust.js} +2 -16
- package/dist/generator/emit-shared-rust.js.map +1 -0
- package/dist/generator/emit-shared-typescript.d.ts +12 -0
- package/dist/generator/emit-shared-typescript.d.ts.map +1 -0
- package/dist/generator/{emit-straightline-typescript.js → emit-shared-typescript.js} +5 -20
- package/dist/generator/emit-shared-typescript.js.map +1 -0
- package/dist/generator/emit-straightline-typed-typescript.d.ts +1 -1
- package/dist/generator/emit-straightline-typed-typescript.d.ts.map +1 -1
- package/dist/generator/emit-straightline-typed-typescript.js +1 -1
- package/dist/generator/emit-straightline-typed-typescript.js.map +1 -1
- package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-go.js +546 -82
- package/dist/generator/emit-typed-native-go.js.map +1 -1
- package/dist/generator/emit-typed-native-rust.d.ts +2 -1
- package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-rust.js +597 -110
- package/dist/generator/emit-typed-native-rust.js.map +1 -1
- package/dist/generator/index.d.ts +2 -12
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +10 -62
- package/dist/generator/index.js.map +1 -1
- package/dist/generator/native-expr.d.ts +120 -0
- package/dist/generator/native-expr.d.ts.map +1 -0
- package/dist/generator/native-expr.js +456 -0
- package/dist/generator/native-expr.js.map +1 -0
- package/dist/generator/typed.d.ts.map +1 -1
- package/dist/generator/typed.js +8 -0
- package/dist/generator/typed.js.map +1 -1
- package/dist/guard.js +14 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plan.d.ts +6 -2
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +4 -0
- package/dist/plan.js.map +1 -1
- package/package.json +1 -1
- package/dist/generator/emit-go.d.ts +0 -19
- package/dist/generator/emit-go.d.ts.map +0 -1
- package/dist/generator/emit-go.js +0 -79
- package/dist/generator/emit-go.js.map +0 -1
- package/dist/generator/emit-raw-abi-go.d.ts +0 -45
- package/dist/generator/emit-raw-abi-go.d.ts.map +0 -1
- package/dist/generator/emit-raw-abi-go.js +0 -346
- package/dist/generator/emit-raw-abi-go.js.map +0 -1
- package/dist/generator/emit-raw-abi-rust.d.ts +0 -45
- package/dist/generator/emit-raw-abi-rust.d.ts.map +0 -1
- package/dist/generator/emit-raw-abi-rust.js +0 -380
- package/dist/generator/emit-raw-abi-rust.js.map +0 -1
- package/dist/generator/emit-rust.d.ts +0 -25
- package/dist/generator/emit-rust.d.ts.map +0 -1
- package/dist/generator/emit-rust.js +0 -125
- package/dist/generator/emit-rust.js.map +0 -1
- package/dist/generator/emit-straightline-go.d.ts +0 -60
- package/dist/generator/emit-straightline-go.d.ts.map +0 -1
- package/dist/generator/emit-straightline-go.js.map +0 -1
- package/dist/generator/emit-straightline-native-go.d.ts +0 -54
- package/dist/generator/emit-straightline-native-go.d.ts.map +0 -1
- package/dist/generator/emit-straightline-native-go.js +0 -835
- package/dist/generator/emit-straightline-native-go.js.map +0 -1
- package/dist/generator/emit-straightline-native-rust.d.ts +0 -27
- package/dist/generator/emit-straightline-native-rust.d.ts.map +0 -1
- package/dist/generator/emit-straightline-native-rust.js +0 -624
- package/dist/generator/emit-straightline-native-rust.js.map +0 -1
- package/dist/generator/emit-straightline-php.d.ts +0 -46
- package/dist/generator/emit-straightline-php.d.ts.map +0 -1
- package/dist/generator/emit-straightline-php.js +0 -941
- package/dist/generator/emit-straightline-php.js.map +0 -1
- package/dist/generator/emit-straightline-python.d.ts +0 -36
- package/dist/generator/emit-straightline-python.d.ts.map +0 -1
- package/dist/generator/emit-straightline-python.js +0 -855
- package/dist/generator/emit-straightline-python.js.map +0 -1
- package/dist/generator/emit-straightline-rust.d.ts.map +0 -1
- package/dist/generator/emit-straightline-rust.js.map +0 -1
- package/dist/generator/emit-straightline-typed-go.d.ts.map +0 -1
- package/dist/generator/emit-straightline-typed-go.js +0 -1114
- package/dist/generator/emit-straightline-typed-go.js.map +0 -1
- package/dist/generator/emit-straightline-typed-rust.d.ts.map +0 -1
- package/dist/generator/emit-straightline-typed-rust.js +0 -1155
- package/dist/generator/emit-straightline-typed-rust.js.map +0 -1
- package/dist/generator/emit-straightline-typescript.d.ts +0 -47
- package/dist/generator/emit-straightline-typescript.d.ts.map +0 -1
- package/dist/generator/emit-straightline-typescript.js.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { GeneratorFailure } from "./core.js";
|
|
2
|
-
import { goStraightlineInternals } from "./emit-
|
|
3
|
-
import { goTypedInternals } from "./emit-
|
|
2
|
+
import { goStraightlineInternals } from "./emit-shared-go.js";
|
|
3
|
+
import { goTypedInternals } from "./emit-shared-go-typed.js";
|
|
4
4
|
import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
|
|
5
5
|
import { concurrencyPlan, execPlan, analyzeSequentialOps, classifyExpr, buildComponentPlan, } from "./straightline.js";
|
|
6
|
+
import { NativeExprCompiler } from "./native-expr.js";
|
|
6
7
|
const PKG = "dslcontracts";
|
|
7
8
|
/** goErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
|
|
8
9
|
* plane (runtime-free). The covered module declares `type BehaviorError struct { Code, Message string }`
|
|
@@ -47,7 +48,7 @@ function portsStructName(compName, nodeId) {
|
|
|
47
48
|
// type to implement the per-component Handler_<comp> interface's concrete Node_* method signatures.
|
|
48
49
|
return `PortsNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
49
50
|
}
|
|
50
|
-
function inferPortType(node, inputPorts) {
|
|
51
|
+
function inferPortType(node, inputPorts, asBinding) {
|
|
51
52
|
if (staticArrElems(node) !== null)
|
|
52
53
|
return "Value";
|
|
53
54
|
const e = classifyExpr(node);
|
|
@@ -58,6 +59,18 @@ function inferPortType(node, inputPorts) {
|
|
|
58
59
|
case "bool":
|
|
59
60
|
return "bool";
|
|
60
61
|
case "ref": {
|
|
62
|
+
// #108: a `$as`-headed ref (map element binding) — resolve the element field's scalar type.
|
|
63
|
+
if (asBinding !== undefined && e.path[0] === asBinding.name) {
|
|
64
|
+
try {
|
|
65
|
+
const { ref } = goTypedInternals.typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan);
|
|
66
|
+
if (ref.kind === "scalar" && ref.scalar !== "null")
|
|
67
|
+
return goScalarPortField(ref.scalar);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return "Value";
|
|
71
|
+
}
|
|
72
|
+
return "Value";
|
|
73
|
+
}
|
|
61
74
|
if (e.path.length === 1) {
|
|
62
75
|
const s = inputPorts[e.path[0]];
|
|
63
76
|
if (s) {
|
|
@@ -77,6 +90,10 @@ function inferPortType(node, inputPorts) {
|
|
|
77
90
|
return "Value";
|
|
78
91
|
}
|
|
79
92
|
}
|
|
93
|
+
/** scalar TypeRef → PortFieldType (native go scalar kind). */
|
|
94
|
+
function goScalarPortField(scalar) {
|
|
95
|
+
return scalar === "string" ? "string" : scalar === "int" ? "int64" : scalar === "float" ? "float64" : "bool";
|
|
96
|
+
}
|
|
80
97
|
/** A static string-array port (every element a static string literal) — the graphddb `projection`
|
|
81
98
|
* shape. Returns the list of literal strings, or null when the node is not that shape. */
|
|
82
99
|
function staticStringArrElems(node) {
|
|
@@ -102,13 +119,17 @@ function staticStringArrElems(node) {
|
|
|
102
119
|
* Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no
|
|
103
120
|
* `dslcontracts.Value` fallback on the native module). `where` names the emission site for the error.
|
|
104
121
|
*/
|
|
105
|
-
function portFieldGoType(node, inputPorts, where = "port") {
|
|
122
|
+
function portFieldGoType(node, inputPorts, where = "port", asBinding, plan) {
|
|
106
123
|
if (staticStringArrElems(node) !== null)
|
|
107
124
|
return "[]string";
|
|
125
|
+
// #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → []ElemT.
|
|
126
|
+
const arrElem = portArrayElemRef(node, inputPorts, plan);
|
|
127
|
+
if (arrElem !== null)
|
|
128
|
+
return `[]${renderTypeRef(arrElem)}`;
|
|
108
129
|
const num = numLiteralGoExpr(node);
|
|
109
130
|
if (num !== null)
|
|
110
131
|
return Number.isInteger(node) ? "int64" : "float64";
|
|
111
|
-
const ft = inferPortType(node, inputPorts);
|
|
132
|
+
const ft = inferPortType(node, inputPorts, asBinding);
|
|
112
133
|
if (ft === "Value") {
|
|
113
134
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): ${where} '${JSON.stringify(node)}' does not lower to a native Go type (would need a boxed dslcontracts.Value). The covered read plane is runtime-free and admits NO boxed escape — the covered corpus ports are string / bool / static-string-array (projection) / bare-number (limit) only. Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
|
|
114
135
|
}
|
|
@@ -123,6 +144,36 @@ function staticArrElems(node) {
|
|
|
123
144
|
const arg = node.arr;
|
|
124
145
|
return Array.isArray(arg) ? arg : null;
|
|
125
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* portArrayElemRef (#110) — resolve a componentRef input port that binds a RUNTIME ARRAY (an IN-list /
|
|
149
|
+
* array-bound WHERE head) to its native element TypeRef, or null when the port is not a covered array.
|
|
150
|
+
* The covered shape is a SINGLE-SEGMENT, non-opt `ref` into an INPUT ARRAY port that carries a resolvable
|
|
151
|
+
* `elemType` (scp-ir-architecture.md §5.2 / PortSchema.elemType — BC does NOT infer, consumer-interface
|
|
152
|
+
* C3). The port then lowers to a native `[]ElemT` field fed straight from `in.<Field>` — ZERO boxed Value
|
|
153
|
+
* on the read hot path. This REUSES the bc#108 `elemType` element lowering (deriveTypeRef over the input
|
|
154
|
+
* port's elemType), extended from the `map…over` element-source path to a plain componentRef port.
|
|
155
|
+
*
|
|
156
|
+
* A static string array is a DIFFERENT covered shape ([]string, staticStringArrElems) — handled ahead of
|
|
157
|
+
* this. An array port with NO resolvable elemType, an opt/multi-segment ref, or a non-array input schema
|
|
158
|
+
* resolves to null here → the caller keeps its existing fail-closed behaviour (never a silent box).
|
|
159
|
+
* Requires a TypePlan (an obj elemType maps to a named decl); without one it stays null (fail-closed).
|
|
160
|
+
*/
|
|
161
|
+
function portArrayElemRef(node, inputPorts, plan) {
|
|
162
|
+
if (plan === undefined)
|
|
163
|
+
return null;
|
|
164
|
+
if (staticStringArrElems(node) !== null)
|
|
165
|
+
return null; // the static-string-array port ([]string) — not this shape.
|
|
166
|
+
const e = classifyExpr(node);
|
|
167
|
+
if (e.kind !== "ref" || e.opt || e.path.length !== 1)
|
|
168
|
+
return null;
|
|
169
|
+
const schema = inputPorts?.[e.path[0]];
|
|
170
|
+
if (schema === undefined || (schema.type !== "array" && schema.type !== "arr"))
|
|
171
|
+
return null;
|
|
172
|
+
const et = schema.elemType;
|
|
173
|
+
if (et === undefined)
|
|
174
|
+
return null;
|
|
175
|
+
return deriveTypeRef(et, plan);
|
|
176
|
+
}
|
|
126
177
|
/**
|
|
127
178
|
* isNativeRawComponent — a component covered by the combined read emitter. Strictly sequential
|
|
128
179
|
* (no real concurrency), body is ALL componentRef (no map/cond), default 'fail'/'continue'
|
|
@@ -236,27 +287,35 @@ function isNativeRaw(comp) {
|
|
|
236
287
|
* element struct by copying the over element's typed fields + materializing the into field from the
|
|
237
288
|
* aligned batch raw — NO boxed Value on any plane.
|
|
238
289
|
*/
|
|
239
|
-
function coveredMapNode(n, bodyIds) {
|
|
290
|
+
function coveredMapNode(n, bodyIds, comp) {
|
|
240
291
|
if (n === null || typeof n !== "object" || !("map" in n))
|
|
241
292
|
return false;
|
|
242
293
|
const m = n.map;
|
|
243
|
-
// #86 pt2: batched map...into. #93 shape 2:
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
if (m.when !== undefined)
|
|
294
|
+
// #86 pt2: batched map...into. #93 shape 2/3: (non-)batched map...into / connection-collect. #108:
|
|
295
|
+
// a guarded map (`when`) IS covered when it has NO `into` (guard+into is heterogeneous — skipped
|
|
296
|
+
// elements lack the into field, cannot de-box into one []T; keep it fail-closed, matching the typed
|
|
297
|
+
// straightline reject). A guarded map over a native-typed element collection drops skipped elements.
|
|
298
|
+
if (m.when !== undefined && m.into !== undefined)
|
|
248
299
|
return false;
|
|
249
300
|
if (m.policy !== undefined && m.policy !== "fail")
|
|
250
301
|
return false;
|
|
251
302
|
if (m.relationKind !== undefined && m.relationKind !== "single" && m.relationKind !== "connection")
|
|
252
303
|
return false;
|
|
253
|
-
// `over` must be a ref to a PRIOR BODY NODE's typed arr field
|
|
254
|
-
//
|
|
255
|
-
// not
|
|
256
|
-
// through to the boxed typed / straight-line path.
|
|
304
|
+
// `over` element type must be STATICALLY resolvable: a ref to a PRIOR BODY NODE's typed arr field
|
|
305
|
+
// (mapOverElemType) OR a ref to an INPUT array port that carries `elemType` (#108 — the C3 "BC does
|
|
306
|
+
// not infer" rule: an input array WITHOUT elemType stays fail-closed / uncovered).
|
|
257
307
|
const overE = classifyExpr(m.over);
|
|
258
|
-
if (overE.kind !== "ref"
|
|
308
|
+
if (overE.kind !== "ref")
|
|
259
309
|
return false;
|
|
310
|
+
const head = overE.path[0];
|
|
311
|
+
if (!bodyIds.has(head)) {
|
|
312
|
+
// input-array over: covered only when the input port declares elemType (#108).
|
|
313
|
+
if (overE.path.length !== 1)
|
|
314
|
+
return false;
|
|
315
|
+
const schema = comp.inputPorts?.[head];
|
|
316
|
+
if (schema === undefined || schema.elemType === undefined)
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
260
319
|
const ports = m.ports;
|
|
261
320
|
for (const p of Object.keys(ports))
|
|
262
321
|
if (!portIsStatic(ports[p]))
|
|
@@ -295,13 +354,21 @@ function coverageRejectReason(comp) {
|
|
|
295
354
|
}
|
|
296
355
|
const bodyIds = new Set(comp.body.map((b) => b.id));
|
|
297
356
|
for (const n of comp.body) {
|
|
298
|
-
if ("cond" in n)
|
|
299
|
-
|
|
357
|
+
if ("cond" in n) {
|
|
358
|
+
// #108: a cond node is native-covered — its `if`/`then`/`else` compile to native expressions via
|
|
359
|
+
// the runtime-free NativeExprCompiler (byte-equal to run_behavior's cond lower). If an expression
|
|
360
|
+
// is not statically compilable (uncovered op / refOpt / opt operand / type mismatch), the compiler
|
|
361
|
+
// fails closed at emit (a loud GeneratorFailure) — never a silent box. cond requires outType.
|
|
362
|
+
if (n.outType === undefined)
|
|
363
|
+
return `node '${n.id}' cond has no outType (required to de-box the branch join)`;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
300
366
|
if ("map" in n) {
|
|
301
|
-
// #86 pt2 batched map...into / #93 shape 2
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
367
|
+
// #86 pt2 batched map...into / #93 shape 2/3 / #108 guarded map (no into) / map-over-input (with
|
|
368
|
+
// elemType) ARE covered. A non-covered map shape (guard+into heterogeneous / over-input without
|
|
369
|
+
// elemType) falls through to the loud fail-closed reject.
|
|
370
|
+
if (!coveredMapNode(n, bodyIds, comp))
|
|
371
|
+
return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
|
|
305
372
|
continue;
|
|
306
373
|
}
|
|
307
374
|
if (!("component" in n))
|
|
@@ -342,13 +409,13 @@ function assertTypedCoverage(comp) {
|
|
|
342
409
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#77): component '${comp.name}' is a sequential componentRef read but lacks outType/outputType on every node — the combined emitter de-boxes the RESULT into the node's outType struct, so a covered read MUST be fully typed (bc#45). Add outType annotations, or regenerate on go-straightline-native (boxed result).`);
|
|
343
410
|
}
|
|
344
411
|
// ── native ports struct + PortReader ──────────────────────────────────────────────────
|
|
345
|
-
function emitPortsStruct(comp, node) {
|
|
412
|
+
function emitPortsStruct(comp, node, asBinding, plan) {
|
|
346
413
|
const structName = portsStructName(comp.name, node.id);
|
|
347
414
|
const portNames = Object.keys(node.ports);
|
|
348
415
|
if (portNames.length === 0) {
|
|
349
416
|
return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
|
|
350
417
|
}
|
|
351
|
-
const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp.inputPorts));
|
|
418
|
+
const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding, plan));
|
|
352
419
|
const fieldNames = portNames.map(goPortFieldName);
|
|
353
420
|
const fields = portNames
|
|
354
421
|
.map((p, i) => `\t${fieldNames[i]} ${fieldTypes[i]} // ${JSON.stringify(p)}`)
|
|
@@ -391,6 +458,14 @@ function rawElemStructName(compName, nodeId) {
|
|
|
391
458
|
function handlerIfaceName(compName) {
|
|
392
459
|
return `Handler_${sanitize(compName)}`;
|
|
393
460
|
}
|
|
461
|
+
/** RunNativeRawStruct_<comp> — the EXPORTED name of the per-component struct-returning combined runner
|
|
462
|
+
* (bc#102). It MUST be an exported Go identifier so a consumer in a DIFFERENT package can call it (a
|
|
463
|
+
* leading-lowercase name is unexported / uncallable cross-package). The `RunNativeRawStruct_` prefix
|
|
464
|
+
* guarantees the leading char is uppercase regardless of how the component name sanitizes, and the
|
|
465
|
+
* sanitized component suffix keeps it unique per covered component. */
|
|
466
|
+
function runnerName(compName) {
|
|
467
|
+
return `RunNativeRawStruct_${sanitize(compName)}`;
|
|
468
|
+
}
|
|
394
469
|
/** Node_<comp>_<nodeId> — the concrete per-node handler method name. */
|
|
395
470
|
function nodeMethodName(compName, nodeId) {
|
|
396
471
|
return `Node_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
@@ -404,6 +479,9 @@ function nodeMethodName(compName, nodeId) {
|
|
|
404
479
|
function emitRawRowStructs(comp, typedNodes, plan) {
|
|
405
480
|
const parts = [];
|
|
406
481
|
for (const n of comp.body) {
|
|
482
|
+
// #108: a cond node has NO handler-result row (pure Expression — no dispatch).
|
|
483
|
+
if ("cond" in n)
|
|
484
|
+
continue;
|
|
407
485
|
const ref = typedNodes.get(n.id);
|
|
408
486
|
if ("map" in n) {
|
|
409
487
|
// a covered map node: RawElem_<comp>_<node> is the HANDLER'S per-element return shape:
|
|
@@ -498,6 +576,8 @@ function emitHandlerInterface(comp, typedNodes, plan) {
|
|
|
498
576
|
lines.push(`// Node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
|
|
499
577
|
lines.push(`type ${handlerIfaceName(comp.name)} interface {`);
|
|
500
578
|
for (const n of comp.body) {
|
|
579
|
+
if ("cond" in n)
|
|
580
|
+
continue; // #108: a cond node has no handler method (pure Expression).
|
|
501
581
|
const method = nodeMethodName(comp.name, n.id);
|
|
502
582
|
const bt = boundGoType(n, comp, typedNodes, plan);
|
|
503
583
|
if ("map" in n) {
|
|
@@ -524,10 +604,15 @@ function mapElemMaterializerName(compName, nodeId) {
|
|
|
524
604
|
* slice of the concrete per-element ports structs (NO `[]dslcontracts.Value`, NO PortReader accessor);
|
|
525
605
|
* the consumer reads `bp.Items[i].F_PK` directly.
|
|
526
606
|
*/
|
|
527
|
-
function emitMapPortsStructs(comp, node) {
|
|
607
|
+
function emitMapPortsStructs(comp, node, typedNodes, plan) {
|
|
528
608
|
const m = node.map;
|
|
529
609
|
const structName = portsStructName(comp.name, node.id);
|
|
530
|
-
|
|
610
|
+
// #108: the element ports may read the `$as` binding ($id / $g.field) — resolve the over element
|
|
611
|
+
// type so those ports type their struct field natively (map-over-input scalar element / prior-node
|
|
612
|
+
// struct element). A pure-static/concat port ignores the binding.
|
|
613
|
+
const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
614
|
+
const asBinding = { name: m.as, ref: elemRef, plan };
|
|
615
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan);
|
|
531
616
|
const batchStruct = `${structName}_batch`;
|
|
532
617
|
return `${elemStruct}
|
|
533
618
|
|
|
@@ -596,19 +681,35 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
|
596
681
|
}
|
|
597
682
|
return parts.join("\n\n");
|
|
598
683
|
}
|
|
599
|
-
/**
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
|
|
684
|
+
/**
|
|
685
|
+
* mapOverElemInfo — resolve a covered map's `over` collection: its element TypeRef + the Go expression
|
|
686
|
+
* that yields the native over slice. Two sources (#108):
|
|
687
|
+
* - a ref to a PRIOR BODY NODE's typed arr field (`t_root.Groups`) — element type from the parent struct;
|
|
688
|
+
* - a ref to an INPUT ARRAY port carrying `elemType` (`in.Ids`) — element type from PortSchema.elemType.
|
|
689
|
+
* A map over an input array WITHOUT elemType is fail-closed (BC does not infer — consumer-interface C3).
|
|
690
|
+
*/
|
|
691
|
+
function mapOverElemInfo(comp, node, typedNodes, plan) {
|
|
603
692
|
const m = node.map;
|
|
604
693
|
const e = classifyExpr(m.over);
|
|
605
694
|
if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
|
|
606
695
|
const baseRef = typedNodes.get(e.path[0]);
|
|
607
|
-
const { ref } = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
|
|
696
|
+
const { expr, ref } = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
|
|
608
697
|
if (ref.kind === "arr")
|
|
609
|
-
return
|
|
698
|
+
return { elemRef: ref.elem, overExpr: expr };
|
|
610
699
|
}
|
|
611
|
-
|
|
700
|
+
// input-array over (#108): the head is an input port; its declared elemType is the element type.
|
|
701
|
+
if (e.kind === "ref" && e.path.length === 1) {
|
|
702
|
+
const schema = comp.inputPorts?.[e.path[0]];
|
|
703
|
+
const et = schema?.elemType;
|
|
704
|
+
if (et !== undefined) {
|
|
705
|
+
return { elemRef: deriveTypeRef(et, plan), overExpr: `in.${goFieldName(e.path[0])}` };
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined map (#108): map '${node.id}' 'over' must be a ref to a prior node's typed arr field OR an input array port with a declared elemType (got ${JSON.stringify(m.over)}). BC does not infer input-array element types (consumer-interface C3).`);
|
|
709
|
+
}
|
|
710
|
+
/** The Go type of the over-element (back-compat shim). */
|
|
711
|
+
function mapOverElemType(comp, node, typedNodes, plan) {
|
|
712
|
+
return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
|
|
612
713
|
}
|
|
613
714
|
function classifyPort(node) {
|
|
614
715
|
if (staticArrElems(node) !== null)
|
|
@@ -663,8 +764,8 @@ function reconstructG(e) {
|
|
|
663
764
|
function inStructName(compName) {
|
|
664
765
|
return `In_${sanitize(compName)}`;
|
|
665
766
|
}
|
|
666
|
-
/** Go type for an input port's declared type (scalar → concrete;
|
|
667
|
-
function inputPortGoType(schema) {
|
|
767
|
+
/** Go type for an input port's declared type (scalar → concrete; array+elemType → []ElemT; else Value). */
|
|
768
|
+
function inputPortGoType(schema, plan) {
|
|
668
769
|
switch (schema?.type) {
|
|
669
770
|
case "string":
|
|
670
771
|
return "string";
|
|
@@ -674,6 +775,15 @@ function inputPortGoType(schema) {
|
|
|
674
775
|
return "float64";
|
|
675
776
|
case "bool":
|
|
676
777
|
return "bool";
|
|
778
|
+
case "array":
|
|
779
|
+
case "arr": {
|
|
780
|
+
// #108: an input ARRAY port with a declared elemType lowers to a native []ElemT (the map-over-input
|
|
781
|
+
// $as element type source). Without elemType or a plan, it stays a boxed Value (uncovered / fail-closed).
|
|
782
|
+
const et = schema.elemType;
|
|
783
|
+
if (et !== undefined && plan !== undefined)
|
|
784
|
+
return `[]${renderTypeRef(deriveTypeRef(et, plan))}`;
|
|
785
|
+
return `${PKG}.Value`;
|
|
786
|
+
}
|
|
677
787
|
default:
|
|
678
788
|
return `${PKG}.Value`;
|
|
679
789
|
}
|
|
@@ -695,14 +805,14 @@ function inputScalarKind(schema) {
|
|
|
695
805
|
}
|
|
696
806
|
/** emitInStruct — the concrete input struct declaration for a covered component (fields = inputPorts,
|
|
697
807
|
* declaration order = Object key order; gofmt-aligned). Empty inputPorts => empty struct. */
|
|
698
|
-
function emitInStruct(comp) {
|
|
808
|
+
function emitInStruct(comp, plan) {
|
|
699
809
|
const name = inStructName(comp.name);
|
|
700
810
|
const ports = Object.keys(comp.inputPorts ?? {});
|
|
701
811
|
if (ports.length === 0) {
|
|
702
812
|
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
|
|
703
813
|
}
|
|
704
814
|
const names = ports.map((p) => goFieldName(p));
|
|
705
|
-
const types = ports.map((p) => inputPortGoType(comp.inputPorts[p]));
|
|
815
|
+
const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan));
|
|
706
816
|
const nameW = Math.max(0, ...names.map((n) => n.length));
|
|
707
817
|
const typeW = Math.max(0, ...types.map((t) => t.length));
|
|
708
818
|
const fields = ports
|
|
@@ -722,7 +832,7 @@ ${fields}
|
|
|
722
832
|
* pre-built by the consumer. A missing key leaves the zero value (every covered input is required +
|
|
723
833
|
* always supplied by the corpus); a present-but-wrong-scalar is a fail-closed decode error.
|
|
724
834
|
*/
|
|
725
|
-
function emitInDecoder(comp) {
|
|
835
|
+
function emitInDecoder(comp, plan) {
|
|
726
836
|
const name = inStructName(comp.name);
|
|
727
837
|
const ports = Object.keys(comp.inputPorts ?? {});
|
|
728
838
|
const lines = [];
|
|
@@ -739,8 +849,28 @@ function emitInDecoder(comp) {
|
|
|
739
849
|
lines.push(`\t}`);
|
|
740
850
|
for (const p of ports) {
|
|
741
851
|
const field = goFieldName(p);
|
|
742
|
-
const
|
|
852
|
+
const schema = comp.inputPorts[p];
|
|
853
|
+
const scalar = inputScalarKind(schema);
|
|
854
|
+
const et = schema.elemType;
|
|
743
855
|
lines.push(`\tif v, ok := input.Get(${JSON.stringify(p)}); ok {`);
|
|
856
|
+
if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
857
|
+
// #108: decode the boxed Value array into the native []ElemT (test glue — materialize each element
|
|
858
|
+
// via the typed materializer; a non-array or non-conforming element is a fail-closed decode error).
|
|
859
|
+
const elemRef = deriveTypeRef(et, plan);
|
|
860
|
+
const elemGo = renderTypeRef(elemRef);
|
|
861
|
+
lines.push(`\t\tarr, isArr := v.([]${PKG}.Value)`);
|
|
862
|
+
lines.push(`\t\tif !isArr {`);
|
|
863
|
+
lines.push(`\t\t\treturn in, ${PKG}.NewExprFailure("TYPE_MISMATCH", "input ${p}: expected array, got "+${PKG}.TypeName(v))`);
|
|
864
|
+
lines.push(`\t\t}`);
|
|
865
|
+
lines.push(`\t\tdec := make([]${elemGo}, 0, len(arr))`);
|
|
866
|
+
lines.push(`\t\tfor _, ev := range arr {`);
|
|
867
|
+
// materializeExpr returns the must*-based decode (panics on a malformed corpus element — test glue).
|
|
868
|
+
lines.push(`\t\t\tdec = append(dec, ${goTypedInternals.materializeExpr("ev", elemRef, plan)})`);
|
|
869
|
+
lines.push(`\t\t}`);
|
|
870
|
+
lines.push(`\t\tin.${field} = dec`);
|
|
871
|
+
lines.push(`\t}`);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
744
874
|
if (scalar === undefined) {
|
|
745
875
|
lines.push(`\t\tin.${field} = v`);
|
|
746
876
|
}
|
|
@@ -812,8 +942,18 @@ function emitNativeScalar(node, want, resolveRef) {
|
|
|
812
942
|
* plane — a boxed escape would re-introduce the bc-runtime coupling this module removes). Returns the
|
|
813
943
|
* struct field initializer (`Field: <expr>`). `resolveRef` resolves a ref head for the scalar path.
|
|
814
944
|
*/
|
|
815
|
-
function emitPortInit(pn, expr, fieldGoType, resolveRef) {
|
|
945
|
+
function emitPortInit(pn, expr, fieldGoType, resolveRef, inputPorts, plan) {
|
|
816
946
|
const fieldName = goPortFieldName(pn);
|
|
947
|
+
// #110: a componentRef port bound to a runtime array input port (with elemType) → the native []ElemT,
|
|
948
|
+
// read straight from the concrete input struct (`in.<Field>`) — ZERO boxed Value on the read hot path.
|
|
949
|
+
// The over-elem/element-source case (map…over) already resolves via mapOverElemInfo; this is the plain
|
|
950
|
+
// componentRef port (an IN-list / array-bound WHERE head).
|
|
951
|
+
if (inputPorts !== undefined && portArrayElemRef(expr, inputPorts, plan) !== null) {
|
|
952
|
+
const e = classifyExpr(expr);
|
|
953
|
+
if (e.kind === "ref" && e.path.length === 1) {
|
|
954
|
+
return `${fieldName}: in.${goFieldName(e.path[0])}`;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
817
957
|
// change #2: a static string array (projection) → a native `[]string{...}` literal.
|
|
818
958
|
const strArr = staticStringArrElems(expr);
|
|
819
959
|
if (strArr !== null) {
|
|
@@ -899,8 +1039,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
899
1039
|
const resolveRef = resolveRefAt(atPos);
|
|
900
1040
|
for (const pn of Object.keys(node.ports)) {
|
|
901
1041
|
const expr = node.ports[pn];
|
|
902
|
-
const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'
|
|
903
|
-
inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef));
|
|
1042
|
+
const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan);
|
|
1043
|
+
inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef, comp.inputPorts, plan));
|
|
904
1044
|
}
|
|
905
1045
|
lines.push(`${ind}ps_${s} = ${structName}{${inits.join(", ")}}`);
|
|
906
1046
|
lines.push(`${ind}run_${s} = true`);
|
|
@@ -985,7 +1125,7 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
985
1125
|
const outStructType = outRef ? renderTypeRef(outRef) : `${PKG}.Value`;
|
|
986
1126
|
const zeroOut = outRef ? goZero(outRef) : "nil";
|
|
987
1127
|
const lines = [];
|
|
988
|
-
lines.push(`//
|
|
1128
|
+
lines.push(`// ${runnerName(comp.name)} — the STRUCT-RETURNING combined read (bc#77/#87): the fully`);
|
|
989
1129
|
lines.push(`// de-plumbed path. Ports are a native struct (direct field assignment); the handler`);
|
|
990
1130
|
lines.push(`// result is materialized straight into the node's outType struct. Node results are typed`);
|
|
991
1131
|
lines.push(`// struct locals; a relation child reads the parent's REAL struct result via direct field`);
|
|
@@ -1001,7 +1141,7 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1001
1141
|
// whose Node_* methods return the concrete per-node row struct (typed fields = the node's outType).
|
|
1002
1142
|
// The covered result plane carries no boxed ports / boxed value / dynamic accessor / semantic type-
|
|
1003
1143
|
// assert on a row value — the runner reads row.<Field> directly and copies each into the outType local.
|
|
1004
|
-
lines.push(`func
|
|
1144
|
+
lines.push(`func ${runnerName(comp.name)}[H ${handlerIfaceName(comp.name)}](handlers H, in ${inStructName(comp.name)}) (${outStructType}, error) {`);
|
|
1005
1145
|
lines.push(`\t_ = in`);
|
|
1006
1146
|
// resolveNativeRef for this runner: a ref head is either a PRIOR NODE (read its typed struct field
|
|
1007
1147
|
// directly) or an INPUT PORT (read the typed `in.<Field>`). A REQUIRED scalar resolves to a native
|
|
@@ -1057,6 +1197,13 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1057
1197
|
lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut, flags));
|
|
1058
1198
|
continue;
|
|
1059
1199
|
}
|
|
1200
|
+
// #108: a cond node — pure Expression (no handler). Compile `if` to a native bool + materialize the
|
|
1201
|
+
// taken branch to the node's outType, assigning the node's typed cell. Byte-equal to run_behavior's
|
|
1202
|
+
// cond lower ({cond:[if,then,else]}, strict-bool, only the taken branch evaluated).
|
|
1203
|
+
if ("cond" in comp.body[i]) {
|
|
1204
|
+
lines.push(emitCondArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut));
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1060
1207
|
const node = comp.body[i];
|
|
1061
1208
|
const meta = metas[i];
|
|
1062
1209
|
const s = sanitize(node.id);
|
|
@@ -1101,8 +1248,8 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1101
1248
|
const resolveRef = resolveRefAt(k);
|
|
1102
1249
|
for (const pn of portNames) {
|
|
1103
1250
|
const expr = node.ports[pn];
|
|
1104
|
-
const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'
|
|
1105
|
-
inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef));
|
|
1251
|
+
const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan);
|
|
1252
|
+
inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef, comp.inputPorts, plan));
|
|
1106
1253
|
}
|
|
1107
1254
|
lines.push(`${indent}ports_${s} := ${structName}{${inits.join(", ")}}`);
|
|
1108
1255
|
const oc = `row_${s}`;
|
|
@@ -1174,22 +1321,37 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1174
1321
|
const m = node.map;
|
|
1175
1322
|
const batched = m.batched === true;
|
|
1176
1323
|
const hasInto = m.into !== undefined;
|
|
1324
|
+
const hasGuard = m.when !== undefined;
|
|
1177
1325
|
const s = sanitize(node.id);
|
|
1178
1326
|
const structName = portsStructName(comp.name, node.id);
|
|
1179
1327
|
const batchStruct = `${structName}_batch`;
|
|
1180
1328
|
const arrRef = typedNodes.get(node.id);
|
|
1181
1329
|
const elemGo = renderTypeRef(arrRef);
|
|
1182
1330
|
const asName = m.as;
|
|
1183
|
-
|
|
1184
|
-
//
|
|
1185
|
-
const
|
|
1186
|
-
const
|
|
1187
|
-
const overBaseRef = typedNodes.get(overParent);
|
|
1188
|
-
const { expr: overExpr, ref: overRef } = goTypedInternals.typedFieldAccess(typedLocal(overParent), overBaseRef, overE.kind === "ref" ? overE.path.slice(1) : [], plan);
|
|
1189
|
-
const overElemRef = overRef.elem;
|
|
1331
|
+
// over collection element type + the native over-slice expression (#108: prior-node arr OR input array
|
|
1332
|
+
// with elemType). The $as element is a value of `overElemRef` (input-array element or prior-node elem).
|
|
1333
|
+
const { elemRef: overElemRef, overExpr } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
1334
|
+
const overElemGo = renderTypeRef(overElemRef);
|
|
1190
1335
|
const elemMat = mapElemMaterializerName(comp.name, node.id);
|
|
1191
1336
|
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1192
1337
|
const portNames = Object.keys(m.ports);
|
|
1338
|
+
// #108 guard: compile the `when` expression to a native bool over an element-scoped resolveHead
|
|
1339
|
+
// ($as → the over element value `elemVar`; prior node → its typed local; input port → in.<Field>).
|
|
1340
|
+
// Returns the compiled {stmts, expr} or null (no guard). Mirrors run_behavior's per-element keep-gate
|
|
1341
|
+
// ({cond:[when,true,false]}, strict-bool). A non-bool / uncovered guard expr fails closed at emit.
|
|
1342
|
+
const mkGuard = (elemVar) => {
|
|
1343
|
+
if (!hasGuard)
|
|
1344
|
+
return null;
|
|
1345
|
+
const resolveHead = (head) => {
|
|
1346
|
+
if (head === asName)
|
|
1347
|
+
return { expr: elemVar, ref: overElemRef };
|
|
1348
|
+
if (priorHere(head))
|
|
1349
|
+
return { expr: typedLocal(head), ref: typedNodes.get(head) };
|
|
1350
|
+
return goInputHeadRef(head, comp, plan);
|
|
1351
|
+
};
|
|
1352
|
+
const be = makeGoExprBackend(zeroOut, resolveHead, plan);
|
|
1353
|
+
return new NativeExprCompiler(be).compileBool(m.when);
|
|
1354
|
+
};
|
|
1193
1355
|
// emit the per-element native ports struct build (shared; `il` is the indent inside the element
|
|
1194
1356
|
// loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
|
|
1195
1357
|
const buildPorts = (il, elemVar) => {
|
|
@@ -1233,9 +1395,10 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1233
1395
|
return null;
|
|
1234
1396
|
return { expr, scalar: goScalar(acc.scalar) };
|
|
1235
1397
|
};
|
|
1398
|
+
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1236
1399
|
for (const pn of portNames) {
|
|
1237
|
-
const fieldGoType = portFieldGoType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'
|
|
1238
|
-
inits.push(emitPortInit(pn, m.ports[pn], fieldGoType, resolveRef));
|
|
1400
|
+
const fieldGoType = portFieldGoType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding, plan);
|
|
1401
|
+
inits.push(emitPortInit(pn, m.ports[pn], fieldGoType, resolveRef, comp.inputPorts, plan));
|
|
1239
1402
|
}
|
|
1240
1403
|
out.push(`${il}ep_${s} := ${structName}{${inits.join(", ")}}`);
|
|
1241
1404
|
return out;
|
|
@@ -1254,41 +1417,69 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1254
1417
|
lines.push(`${indent}over_${s} := ${overExpr}`);
|
|
1255
1418
|
lines.push(`${indent}${typedLocal(node.id)} = make(${elemGo}, 0, len(over_${s}))`);
|
|
1256
1419
|
const elemRowRef = mapElemRowRef(node, arrRef, plan);
|
|
1420
|
+
// emit the per-element guard keep-gate (#108). `il` = indent, `elemVar` = the over element loop var.
|
|
1421
|
+
// On skip: batched → `continue` (element excluded from items + keptOver); non-batched → `continue`.
|
|
1422
|
+
const emitGuard = (il, elemVar) => {
|
|
1423
|
+
const g = mkGuard(elemVar);
|
|
1424
|
+
if (g === null)
|
|
1425
|
+
return [];
|
|
1426
|
+
const out = [];
|
|
1427
|
+
for (const st of g.stmts)
|
|
1428
|
+
out.push(`${il}${st}`);
|
|
1429
|
+
out.push(`${il}if !(${g.expr}) {`);
|
|
1430
|
+
out.push(`${il}\tcontinue`);
|
|
1431
|
+
out.push(`${il}}`);
|
|
1432
|
+
return out;
|
|
1433
|
+
};
|
|
1257
1434
|
if (batched) {
|
|
1258
|
-
// BATCHED (#86 pt2 into / #93 shape 3 no-into): build
|
|
1259
|
-
// Node_* returns
|
|
1435
|
+
// BATCHED (#86 pt2 into / #93 shape 3 no-into / #108 guarded no-into): build kept-element ports,
|
|
1436
|
+
// dispatch ONCE. The concrete Node_* returns RawRow{IsError,Err,Rows []RawElem} aligned to items.
|
|
1437
|
+
// With a guard, `items` (and, for into, `keptOver`) hold only kept elements — matching run_behavior's
|
|
1438
|
+
// keptIdx-aligned batch. `into` is never combined with a guard (rejected). no-into collects each row.
|
|
1260
1439
|
lines.push(`${indent}items_${s} := make([]${structName}, 0, len(over_${s}))`);
|
|
1440
|
+
if (hasInto)
|
|
1441
|
+
lines.push(`${indent}keptOver_${s} := make([]${overElemGo}, 0, len(over_${s}))`);
|
|
1261
1442
|
lines.push(`${indent}for _, ${elemLocal(s)} := range over_${s} {`);
|
|
1443
|
+
for (const l of emitGuard(indent + "\t", elemLocal(s)))
|
|
1444
|
+
lines.push(l);
|
|
1262
1445
|
for (const l of buildPorts(indent + "\t", elemLocal(s)))
|
|
1263
1446
|
lines.push(l);
|
|
1264
1447
|
lines.push(`${indent}\titems_${s} = append(items_${s}, ep_${s})`);
|
|
1448
|
+
if (hasInto)
|
|
1449
|
+
lines.push(`${indent}\tkeptOver_${s} = append(keptOver_${s}, ${elemLocal(s)})`);
|
|
1265
1450
|
lines.push(`${indent}}`);
|
|
1266
|
-
// empty
|
|
1451
|
+
// empty items => empty result (handler not called), matching run_behavior.
|
|
1267
1452
|
lines.push(`${indent}if len(items_${s}) > 0 {`);
|
|
1268
1453
|
lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: items_${s}}`);
|
|
1269
1454
|
lines.push(`${indent}\tmo_${s}, mo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
|
|
1270
1455
|
lines.push(`${indent}\tif !mo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${m.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
|
|
1271
1456
|
lines.push(`${indent}\tif mo_${s}.IsError {`, `${indent}\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under 'fail' policy: "+mo_${s}.Err`)}`, `${indent}\t}`);
|
|
1272
1457
|
lines.push(`${indent}\tif len(mo_${s}.Rows) != len(items_${s}) {`, `${indent}\t\treturn ${zeroOut}, ${goErr("MAP_BATCH_RESULT_MISMATCH", `fmt.Sprintf("map '${node.id}': batched handler must return a list aligned to items (want %d)", len(items_${s}))`)}`, `${indent}\t}`);
|
|
1273
|
-
lines.push(`${indent}\tfor mk_${s} := range over_${s} {`);
|
|
1274
1458
|
if (hasInto) {
|
|
1275
|
-
// #86 pt2: zip-attach the into field onto each over element (augmented element copier).
|
|
1276
|
-
lines.push(`${indent}\
|
|
1459
|
+
// #86 pt2: zip-attach the into field onto each kept over element (augmented element copier).
|
|
1460
|
+
lines.push(`${indent}\tfor mk_${s} := range keptOver_${s} {`);
|
|
1461
|
+
lines.push(`${indent}\t\tel_${s} := ${elemMat}(keptOver_${s}[mk_${s}], mo_${s}.Rows[mk_${s}])`);
|
|
1462
|
+
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1463
|
+
lines.push(`${indent}\t}`);
|
|
1277
1464
|
}
|
|
1278
1465
|
else {
|
|
1279
|
-
// #93 shape 3: collect each element DIRECTLY from the
|
|
1466
|
+
// #93 shape 3 / #108: collect each element DIRECTLY from the aligned per-element row (field copy).
|
|
1467
|
+
lines.push(`${indent}\tfor mk_${s} := range mo_${s}.Rows {`);
|
|
1280
1468
|
lines.push(`${indent}\t\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
|
|
1281
1469
|
lines.push(emitRowCopy(`el_${s}`, `mo_${s}.Rows[mk_${s}]`, elemRowRef, plan, indent.length + 2));
|
|
1470
|
+
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1471
|
+
lines.push(`${indent}\t}`);
|
|
1282
1472
|
}
|
|
1283
|
-
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1284
|
-
lines.push(`${indent}\t}`);
|
|
1285
1473
|
lines.push(`${indent}}`);
|
|
1286
1474
|
}
|
|
1287
1475
|
else {
|
|
1288
|
-
// #93 shape 2: NON-batched map
|
|
1289
|
-
// requests, matching run_behavior's per-element loop).
|
|
1476
|
+
// #93 shape 2 / #108: NON-batched map — dispatch the child handler ONCE PER KEPT ELEMENT (N physical
|
|
1477
|
+
// requests, matching run_behavior's per-element loop). A guarded element that fails `when` is skipped
|
|
1478
|
+
// (no dispatch, excluded from the result). The concrete Node_* returns ONE RawElem_.
|
|
1290
1479
|
lines.push(`${indent}for mk_${s} := range over_${s} {`);
|
|
1291
1480
|
lines.push(`${indent}\t${elemLocal(s)} := over_${s}[mk_${s}]`);
|
|
1481
|
+
for (const l of emitGuard(indent + "\t", elemLocal(s)))
|
|
1482
|
+
lines.push(l);
|
|
1292
1483
|
for (const l of buildPorts(indent + "\t", elemLocal(s)))
|
|
1293
1484
|
lines.push(l);
|
|
1294
1485
|
// per-element dispatch (ONE physical request per element). run_behavior passes the over element
|
|
@@ -1317,6 +1508,76 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1317
1508
|
function elemLocal(s) {
|
|
1318
1509
|
return `oel_${s}`;
|
|
1319
1510
|
}
|
|
1511
|
+
/** #108: resolve an INPUT port head for a native-expr ref (cond `if` / map `when` / branches). A scalar
|
|
1512
|
+
* port → `in.<Field>` of the scalar type; an array port WITH elemType → `in.<Field>` of `[]ElemT` (so
|
|
1513
|
+
* `len(in.<Field>)` works); anything else → null (fail-closed). */
|
|
1514
|
+
function goInputHeadRef(head, comp, plan) {
|
|
1515
|
+
const schema = comp.inputPorts?.[head];
|
|
1516
|
+
if (schema === undefined)
|
|
1517
|
+
return null;
|
|
1518
|
+
const sc = inputScalarKind(schema);
|
|
1519
|
+
if (sc !== undefined) {
|
|
1520
|
+
const scalarRef = { kind: "scalar", scalar: sc === "int64" ? "int" : sc === "float64" ? "float" : sc };
|
|
1521
|
+
return { expr: `in.${goFieldName(head)}`, ref: scalarRef };
|
|
1522
|
+
}
|
|
1523
|
+
const et = schema.elemType;
|
|
1524
|
+
if ((schema.type === "array" || schema.type === "arr") && et !== undefined) {
|
|
1525
|
+
return { expr: `in.${goFieldName(head)}`, ref: { kind: "arr", elem: deriveTypeRef(et, plan) } };
|
|
1526
|
+
}
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* emitCondArm — the struct-native exec of a covered cond node (#108). Mirrors run_behavior's cond
|
|
1531
|
+
* lower (behavior.ts:425-429): compile `if` to a native bool, materialize the TAKEN branch to the
|
|
1532
|
+
* node's outType (only the taken branch evaluated — short-circuit), assign the node's typed cell and
|
|
1533
|
+
* set produced. NO handler dispatch (cond is a pure Expression). The `if`/`then`/`else` expressions
|
|
1534
|
+
* compile via the runtime-free NativeExprCompiler (byte-equal to `evaluate`); an uncovered op / opt /
|
|
1535
|
+
* refOpt / type-mismatch fails closed at emit.
|
|
1536
|
+
*/
|
|
1537
|
+
function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut) {
|
|
1538
|
+
const c = node.cond;
|
|
1539
|
+
const s = sanitize(node.id);
|
|
1540
|
+
const nodeRef = typedNodes.get(node.id);
|
|
1541
|
+
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1542
|
+
const resolveHead = (head) => {
|
|
1543
|
+
if (priorHere(head))
|
|
1544
|
+
return { expr: typedLocal(head), ref: typedNodes.get(head) };
|
|
1545
|
+
return goInputHeadRef(head, comp, plan);
|
|
1546
|
+
};
|
|
1547
|
+
const be = makeGoExprBackend(zeroOut, resolveHead, plan);
|
|
1548
|
+
const compiler = new NativeExprCompiler(be);
|
|
1549
|
+
const resolveDecl = (name) => findDecl(plan, name).fields;
|
|
1550
|
+
const cond = compiler.compileBool(c.if);
|
|
1551
|
+
const thenC = compiler.compileTo(c.then, nodeRef, resolveDecl);
|
|
1552
|
+
const elseC = compiler.compileTo(c.else, nodeRef, resolveDecl);
|
|
1553
|
+
const lines = [];
|
|
1554
|
+
lines.push(`\t// ── cond '${node.id}'${c.parent ? " (parent:" + c.parent + ")" : ""} — pure Expression (no handler) ──`);
|
|
1555
|
+
let indent = "\t";
|
|
1556
|
+
const closers = [];
|
|
1557
|
+
if (c.parent !== undefined && priorNodeAt(c.parent, atPos)) {
|
|
1558
|
+
lines.push(`${indent}if produced_${sanitize(c.parent)} {`);
|
|
1559
|
+
closers.unshift(`${indent}}`);
|
|
1560
|
+
indent += "\t";
|
|
1561
|
+
}
|
|
1562
|
+
// the `if` condition (its fallible temps hoisted before the branch — the condition is always evaluated).
|
|
1563
|
+
for (const st of cond.stmts)
|
|
1564
|
+
lines.push(`${indent}${st}`);
|
|
1565
|
+
lines.push(`${indent}if ${cond.expr} {`);
|
|
1566
|
+
// TAKEN = then: only this branch's stmts run (short-circuit ≡ evaluate).
|
|
1567
|
+
for (const st of thenC.stmts)
|
|
1568
|
+
lines.push(`${indent}\t${st}`);
|
|
1569
|
+
lines.push(`${indent}\t${typedLocal(node.id)} = ${thenC.expr}`);
|
|
1570
|
+
lines.push(`${indent}} else {`);
|
|
1571
|
+
for (const st of elseC.stmts)
|
|
1572
|
+
lines.push(`${indent}\t${st}`);
|
|
1573
|
+
lines.push(`${indent}\t${typedLocal(node.id)} = ${elseC.expr}`);
|
|
1574
|
+
lines.push(`${indent}}`);
|
|
1575
|
+
lines.push(`${indent}produced_${s} = true`);
|
|
1576
|
+
void nodeRef;
|
|
1577
|
+
for (const cl of closers)
|
|
1578
|
+
lines.push(cl);
|
|
1579
|
+
return lines.join("\n");
|
|
1580
|
+
}
|
|
1320
1581
|
/**
|
|
1321
1582
|
* emitProducedAwareValue — the #86 part 1 produced-aware output lowering (go). It mirrors the
|
|
1322
1583
|
* boxed-typed emitTypedValue, but a `ref`/`refOpt` whose HEAD is a skippable relationSingle child is
|
|
@@ -1433,9 +1694,190 @@ function goZero(ref) {
|
|
|
1433
1694
|
// bc#90 / runtime-free: the nv* helper set (nvVE/nvBindVE/nvFieldVE/nvCatVE/nvArrVE) is REMOVED —
|
|
1434
1695
|
// those were dslcontracts.Value-based; the covered plane now lowers every port to a native Go value
|
|
1435
1696
|
// (emitPortInit) so no VE helper is emitted, and the module imports ZERO bc runtime.
|
|
1697
|
+
// ── native expression compiler backend (#108) — cond `if` / map `when` / cond branches ──────────
|
|
1698
|
+
//
|
|
1699
|
+
// The runtime-free native expression compiler (native-expr.ts) mirrors runBehavior's cond/guard
|
|
1700
|
+
// Expression evaluation with BYTE-EQUAL semantics. The value-dependent runtime failures (INT_OVERFLOW
|
|
1701
|
+
// / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS / INVALID_LITERAL for a huge len) are emitted as calls to a
|
|
1702
|
+
// FIXED set of RUNTIME-FREE helper funcs baked into the module (bcExpr* below) — the covered module
|
|
1703
|
+
// still imports ZERO bc runtime. A fallible helper returns (T, *BehaviorError); the runner early-returns
|
|
1704
|
+
// the failure (its zero output value is `zeroOut`).
|
|
1705
|
+
let goExprTempSeq = 0;
|
|
1706
|
+
/** module-level accumulator: set true when the native-expr compiler emits a fallible helper call
|
|
1707
|
+
* (so emit() includes the helper library + imports `math`). Reset at the start of each emit(). */
|
|
1708
|
+
const goExprUsed = { helpers: false };
|
|
1709
|
+
/** GoNativeExprBackend — renders native-expr for Go. `zeroOut` is the runner's error-return zero
|
|
1710
|
+
* value; `resolveHead` maps a ref head (input port / input-array $as elem / prior node) to a typed
|
|
1711
|
+
* base expr. Sets goExprUsed.helpers when a fallible helper is emitted (so emit() bakes the helper lib). */
|
|
1712
|
+
function makeGoExprBackend(zeroOut, resolveHead, plan) {
|
|
1713
|
+
const used = goExprUsed;
|
|
1714
|
+
return {
|
|
1715
|
+
lang: "go",
|
|
1716
|
+
freshTemp: () => `ex_${goExprTempSeq++}`,
|
|
1717
|
+
renderType: (ref) => renderTypeRef(ref),
|
|
1718
|
+
scalarType: (s) => goScalar(s),
|
|
1719
|
+
strLit: (s) => JSON.stringify(s),
|
|
1720
|
+
boolLit: (b) => (b ? "true" : "false"),
|
|
1721
|
+
intLit: (dec) => `int64(${dec})`,
|
|
1722
|
+
floatLit: (n) => `float64(${goFloatLitExpr(n)})`,
|
|
1723
|
+
fieldAccess: (baseExpr, baseRef, path) => goTypedInternals.typedFieldAccess(baseExpr, baseRef, path, plan),
|
|
1724
|
+
resolveHead,
|
|
1725
|
+
hoistFallible: (temp, _ty, call) => {
|
|
1726
|
+
used.helpers = true;
|
|
1727
|
+
// a unique err var per hoist (Go `:=` re-declares fine in the same block only for NEW LHS; use a
|
|
1728
|
+
// fresh err name to avoid "no new variables on left side of :=" when several hoists share a block).
|
|
1729
|
+
const errv = `err_${goExprTempSeq++}`;
|
|
1730
|
+
return [`${temp}, ${errv} := ${call}`, `if ${errv} != nil {`, `\treturn ${zeroOut}, ${errv}`, `}`];
|
|
1731
|
+
},
|
|
1732
|
+
helperCall: (helper, args) => {
|
|
1733
|
+
used.helpers = true;
|
|
1734
|
+
return `bcExpr_${helper}(${args.join(", ")})`;
|
|
1735
|
+
},
|
|
1736
|
+
binOp: (op, a, b) => `(${a} ${op} ${b})`,
|
|
1737
|
+
concat: (parts) => {
|
|
1738
|
+
// native string + concat (all parts are native strings) — no import needed.
|
|
1739
|
+
return `(${parts.join(" + ")})`;
|
|
1740
|
+
},
|
|
1741
|
+
notOp: (a) => `(!${a})`,
|
|
1742
|
+
structLit: (name, inits) => `${name}{${inits.map((i) => `${goFieldName(i.field)}: ${i.expr}`).join(", ")}}`,
|
|
1743
|
+
arrLit: (elemTy, items) => `[]${elemTy}{${items.join(", ")}}`,
|
|
1744
|
+
ternary: (cond, t, e, ty) => `func() ${ty} { if ${cond} { return ${t} }; return ${e} }()`,
|
|
1745
|
+
shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
|
|
1746
|
+
// and: temp=false; if left { <rStmts>; temp=rExpr }. or: temp=true; if !left { <rStmts>; temp=rExpr }.
|
|
1747
|
+
const gate = op === "and" ? left : `!(${left})`;
|
|
1748
|
+
const seed = op === "and" ? "false" : "true";
|
|
1749
|
+
return [
|
|
1750
|
+
`var ${temp} bool = ${seed}`,
|
|
1751
|
+
`if ${gate} {`,
|
|
1752
|
+
...rStmts.map((s) => `\t${s}`),
|
|
1753
|
+
`\t${temp} = ${rExpr}`,
|
|
1754
|
+
`}`,
|
|
1755
|
+
];
|
|
1756
|
+
},
|
|
1757
|
+
condGuard: (temp, ty, cond, tStmts, tExpr, eStmts, eExpr) => [
|
|
1758
|
+
`var ${temp} ${ty}`,
|
|
1759
|
+
`if ${cond} {`,
|
|
1760
|
+
...tStmts.map((s) => `\t${s}`),
|
|
1761
|
+
`\t${temp} = ${tExpr}`,
|
|
1762
|
+
`} else {`,
|
|
1763
|
+
...eStmts.map((s) => `\t${s}`),
|
|
1764
|
+
`\t${temp} = ${eExpr}`,
|
|
1765
|
+
`}`,
|
|
1766
|
+
],
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
/** Go float literal (finite; matches goFloatLit convention used elsewhere). */
|
|
1770
|
+
function goFloatLitExpr(n) {
|
|
1771
|
+
if (Number.isInteger(n))
|
|
1772
|
+
return `${n}.0`;
|
|
1773
|
+
return String(n);
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* emitExprHelpers — the FIXED runtime-free native-expr helper library (Go). Emitted once per covered
|
|
1777
|
+
* module that uses any cond/guard/arith. Each helper is BYTE-EQUAL to the reference core in
|
|
1778
|
+
* expr-eval.ts (checkI64 / checkFinite / mod trunc / div widen / code-point compare / lenCore). All
|
|
1779
|
+
* failures are the LOCAL *BehaviorError with the SAME code strings as run_behavior.
|
|
1780
|
+
*/
|
|
1781
|
+
function emitExprHelpers() {
|
|
1782
|
+
return `// ── native-expr runtime-free helper library (#108) — byte-equal to expr-eval.ts core ──
|
|
1783
|
+
const bcI64Max int64 = 9223372036854775807
|
|
1784
|
+
const bcI64Min int64 = -9223372036854775808
|
|
1785
|
+
|
|
1786
|
+
func bcExpr_addI64(a, b int64) (int64, *BehaviorError) {
|
|
1787
|
+
r := a + b
|
|
1788
|
+
// signed overflow detection (a+b): same-sign operands, result flips sign.
|
|
1789
|
+
if (a > 0 && b > 0 && r < 0) || (a < 0 && b < 0 && r > 0) {
|
|
1790
|
+
return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
|
|
1791
|
+
}
|
|
1792
|
+
return r, nil
|
|
1793
|
+
}
|
|
1794
|
+
func bcExpr_subI64(a, b int64) (int64, *BehaviorError) {
|
|
1795
|
+
r := a - b
|
|
1796
|
+
if (a >= 0 && b < 0 && r < 0) || (a < 0 && b > 0 && r >= 0) {
|
|
1797
|
+
return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
|
|
1798
|
+
}
|
|
1799
|
+
return r, nil
|
|
1800
|
+
}
|
|
1801
|
+
func bcExpr_mulI64(a, b int64) (int64, *BehaviorError) {
|
|
1802
|
+
if a == 0 || b == 0 {
|
|
1803
|
+
return 0, nil
|
|
1804
|
+
}
|
|
1805
|
+
r := a * b
|
|
1806
|
+
if r/b != a || (a == bcI64Min && b == -1) {
|
|
1807
|
+
return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
|
|
1808
|
+
}
|
|
1809
|
+
return r, nil
|
|
1810
|
+
}
|
|
1811
|
+
func bcExpr_negI64(a int64) (int64, *BehaviorError) {
|
|
1812
|
+
if a == bcI64Min {
|
|
1813
|
+
return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
|
|
1814
|
+
}
|
|
1815
|
+
return -a, nil
|
|
1816
|
+
}
|
|
1817
|
+
func bcExpr_checkFinite(v float64) (float64, *BehaviorError) {
|
|
1818
|
+
if v != v || v > 1.7976931348623157e308 || v < -1.7976931348623157e308 {
|
|
1819
|
+
return 0, &BehaviorError{Code: "NAN_OR_INF", Message: "non-finite float"}
|
|
1820
|
+
}
|
|
1821
|
+
return v, nil
|
|
1822
|
+
}
|
|
1823
|
+
func bcExpr_addF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a + b) }
|
|
1824
|
+
func bcExpr_subF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a - b) }
|
|
1825
|
+
func bcExpr_mulF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a * b) }
|
|
1826
|
+
func bcExpr_negF(a float64) (float64, *BehaviorError) { return bcExpr_checkFinite(-a) }
|
|
1827
|
+
func bcExpr_modF(a, b float64) (float64, *BehaviorError) { m := math.Mod(a, b); return bcExpr_checkFinite(m) }
|
|
1828
|
+
func bcExpr_modI64(a, b int64) (int64, *BehaviorError) {
|
|
1829
|
+
if b == 0 {
|
|
1830
|
+
return 0, &BehaviorError{Code: "MOD_ZERO", Message: "int mod by zero"}
|
|
1831
|
+
}
|
|
1832
|
+
// Go % is truncated (sign of dividend) — matches modCore.
|
|
1833
|
+
r := a % b
|
|
1834
|
+
// overflow only at MinInt64 % -1 → 0 in Go (no overflow); still checkI64-safe.
|
|
1835
|
+
return r, nil
|
|
1836
|
+
}
|
|
1837
|
+
func bcExpr_widenI64F(a int64) (float64, *BehaviorError) {
|
|
1838
|
+
// div widening exact range is ±2^53 (WIDEN_EXACT). Outside → PRECISION_LOSS.
|
|
1839
|
+
if a > 9007199254740992 || a < -9007199254740992 {
|
|
1840
|
+
return 0, &BehaviorError{Code: "PRECISION_LOSS", Message: "int exceeds exact float range (±2^53)"}
|
|
1841
|
+
}
|
|
1842
|
+
return float64(a), nil
|
|
1843
|
+
}
|
|
1844
|
+
func bcExpr_divF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a / b) }
|
|
1845
|
+
func bcExpr_lenArr[T any](a []T) (int64, *BehaviorError) {
|
|
1846
|
+
return int64(len(a)), nil
|
|
1847
|
+
}
|
|
1848
|
+
// code-point (UTF-8 rune) order comparison — matches cmpCodePoints (NOT UTF-16 / byte compare, which
|
|
1849
|
+
// Go's native string < already is for valid UTF-8, but we compare rune-by-rune to match exactly).
|
|
1850
|
+
func bcExpr_cmpCP(a, b string) int {
|
|
1851
|
+
ra := []rune(a)
|
|
1852
|
+
rb := []rune(b)
|
|
1853
|
+
n := len(ra)
|
|
1854
|
+
if len(rb) < n {
|
|
1855
|
+
n = len(rb)
|
|
1856
|
+
}
|
|
1857
|
+
for i := 0; i < n; i++ {
|
|
1858
|
+
if ra[i] != rb[i] {
|
|
1859
|
+
if ra[i] < rb[i] {
|
|
1860
|
+
return -1
|
|
1861
|
+
}
|
|
1862
|
+
return 1
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
if len(ra) == len(rb) {
|
|
1866
|
+
return 0
|
|
1867
|
+
}
|
|
1868
|
+
if len(ra) < len(rb) {
|
|
1869
|
+
return -1
|
|
1870
|
+
}
|
|
1871
|
+
return 1
|
|
1872
|
+
}
|
|
1873
|
+
func bcExpr_strLt(a, b string) bool { return bcExpr_cmpCP(a, b) < 0 }
|
|
1874
|
+
func bcExpr_strLe(a, b string) bool { return bcExpr_cmpCP(a, b) <= 0 }
|
|
1875
|
+
func bcExpr_strGt(a, b string) bool { return bcExpr_cmpCP(a, b) > 0 }
|
|
1876
|
+
func bcExpr_strGe(a, b string) bool { return bcExpr_cmpCP(a, b) >= 0 }`;
|
|
1877
|
+
}
|
|
1436
1878
|
// ── BindNativeRaw dispatch surface ──────────────────────────────────────────────────────
|
|
1437
1879
|
/** The struct dispatch surface: ComponentNamesNativeRaw + the per-component struct runners
|
|
1438
|
-
* (
|
|
1880
|
+
* (RunNativeRawStruct_<comp>) ARE the exposed API. The consumer calls the struct runner directly
|
|
1439
1881
|
* (it returns the typed model — NO Value serialization on the read hot path, matching how graphddb
|
|
1440
1882
|
* reads: a model, never a boxed Value). There is deliberately NO Value dispatch map — that would
|
|
1441
1883
|
* require a Value serializer (the boxing residue bc#77 eliminates). A canonical Value observation
|
|
@@ -1443,7 +1885,7 @@ function goZero(ref) {
|
|
|
1443
1885
|
* which lives OUTSIDE this module so the covered module stays literal whole-file zero. */
|
|
1444
1886
|
function emitStructSurface(native) {
|
|
1445
1887
|
return `// ComponentNamesNativeRaw — covered reads exposed on the combined struct-native path (declaration
|
|
1446
|
-
// order). Each is driven via
|
|
1888
|
+
// order). Each is driven via RunNativeRawStruct_<comp>(handlers, input) -> (T, error): a STRUCT
|
|
1447
1889
|
// return; the consumer keeps the model native (no Value serialization on the read hot path — the
|
|
1448
1890
|
// boxing residue bc#77 removes). See INTEGRATION.md §6.
|
|
1449
1891
|
var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name)).join(", ")}}`;
|
|
@@ -1453,11 +1895,13 @@ var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name
|
|
|
1453
1895
|
* mismatch Sprintf, `strings` for a concat builder, `sync` for a parallel stage). ExpectedSpecVersions
|
|
1454
1896
|
* stays as a local `var` for provenance, but the old `dslcontracts.SpecVersions` skew `init()` is GONE
|
|
1455
1897
|
* (change #5 — there is no runtime to skew against once the module is runtime-free). */
|
|
1456
|
-
function emitMinimalHeader(ctx, needStrings, needSync, needFmt) {
|
|
1898
|
+
function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath) {
|
|
1457
1899
|
const sv = ctx.specVersions;
|
|
1458
1900
|
const imports = [];
|
|
1459
1901
|
if (needFmt)
|
|
1460
1902
|
imports.push(`\t"fmt"`);
|
|
1903
|
+
if (needMath)
|
|
1904
|
+
imports.push(`\t"math"`);
|
|
1461
1905
|
if (needStrings)
|
|
1462
1906
|
imports.push(`\t"strings"`);
|
|
1463
1907
|
if (needSync)
|
|
@@ -1484,6 +1928,9 @@ function emit(ctx) {
|
|
|
1484
1928
|
const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
|
|
1485
1929
|
const plan = buildTypePlan(ctx.ir);
|
|
1486
1930
|
const decls = emitTypeDecls(plan);
|
|
1931
|
+
// #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler when it emits
|
|
1932
|
+
// a fallible checked-arith helper call — determines whether the helper library + `math` import ship).
|
|
1933
|
+
goExprUsed.helpers = false;
|
|
1487
1934
|
// Emit the runners first so the nv* helper usage flags are known.
|
|
1488
1935
|
const flags = { bind: false, field: false, cat: false, arr: false };
|
|
1489
1936
|
const structs = [];
|
|
@@ -1493,18 +1940,23 @@ function emit(ctx) {
|
|
|
1493
1940
|
const elemCopiers = [];
|
|
1494
1941
|
const runnerCodes = [];
|
|
1495
1942
|
for (const c of native) {
|
|
1496
|
-
inStructs.push(emitInStruct(c));
|
|
1943
|
+
inStructs.push(emitInStruct(c, plan));
|
|
1497
1944
|
const typedNodes = new Map();
|
|
1945
|
+
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
1946
|
+
for (const n of c.body)
|
|
1947
|
+
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1498
1948
|
for (const n of c.body) {
|
|
1499
1949
|
if ("map" in n) {
|
|
1500
|
-
// #86 pt2: a map node emits an element ports struct + a batch struct
|
|
1501
|
-
//
|
|
1502
|
-
structs.push(emitMapPortsStructs(c, n));
|
|
1950
|
+
// #86 pt2 / #108: a map node emits an element ports struct + a batch struct. The element ports
|
|
1951
|
+
// may read the `$as` binding — resolved via the over-element type (prior node / input array).
|
|
1952
|
+
structs.push(emitMapPortsStructs(c, n, typedNodes, plan));
|
|
1953
|
+
}
|
|
1954
|
+
else if ("cond" in n) {
|
|
1955
|
+
// #108: a cond node has NO ports struct (it is a pure Expression — no handler dispatch).
|
|
1503
1956
|
}
|
|
1504
1957
|
else {
|
|
1505
|
-
structs.push(emitPortsStruct(c, n));
|
|
1958
|
+
structs.push(emitPortsStruct(c, n, undefined, plan));
|
|
1506
1959
|
}
|
|
1507
|
-
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1508
1960
|
}
|
|
1509
1961
|
// CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
|
|
1510
1962
|
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
@@ -1539,7 +1991,10 @@ function emit(ctx) {
|
|
|
1539
1991
|
`box uncovered shapes. Implement native coverage for this shape, fix the spec if the shape is ` +
|
|
1540
1992
|
`genuinely runtime-dynamic, or generate this component on the --ir dynamic interpreter surface.`);
|
|
1541
1993
|
}
|
|
1542
|
-
|
|
1994
|
+
// #108: `math` is imported iff the native-expr compiler emitted a fallible helper (bcExpr_modF uses
|
|
1995
|
+
// math.Mod). The helper library itself is baked into the module only when used (goExprUsed.helpers).
|
|
1996
|
+
const needMath = goExprUsed.helpers;
|
|
1997
|
+
const header = emitMinimalHeader(ctx, false, needSync, needFmt, needMath);
|
|
1543
1998
|
const banner = `// COMBINED READ layer (bc#90 — the RUNTIME-FREE read de-box; STRUCT-ONLY, FULLY-NATIVE surface). A
|
|
1544
1999
|
// covered read is a strictly-sequential typed componentRef chain (point reads + relationKind:single
|
|
1545
2000
|
// children), emitted ONLY here: each node builds a native ports struct by direct field assignment (every
|
|
@@ -1551,7 +2006,7 @@ function emit(ctx) {
|
|
|
1551
2006
|
// *BehaviorError (same codes, byte-equal to run_behavior). Exec is INLINE sequential (no plan driver) so
|
|
1552
2007
|
// a relation child reads the parent's REAL struct result via direct field access and the child-present
|
|
1553
2008
|
// decision is made from the real parent value — relationSingle / connection converge with run_behavior
|
|
1554
|
-
// (fixes #323/#74). The exposed API is the STRUCT-returning
|
|
2009
|
+
// (fixes #323/#74). The exposed API is the STRUCT-returning RunNativeRawStruct_<comp> (the consumer
|
|
1555
2010
|
// keeps the model native — no serialization on the read hot path). This module is FULLY NATIVE: a naive
|
|
1556
2011
|
// grep for boxing primitives OR for the bc-runtime package finds nothing, by design (bc#90/runtime-free).`;
|
|
1557
2012
|
const sections = [
|
|
@@ -1559,12 +2014,19 @@ function emit(ctx) {
|
|
|
1559
2014
|
`// Local concrete failure type (runtime-free) — a covered runner returns *BehaviorError (satisfies\n// error) instead of a bc-runtime failure, so the fully-covered module imports ZERO bc runtime.\n${behaviorErrorType}`,
|
|
1560
2015
|
decls ? `// Typed struct declarations (outType-derived; hash-dedup — shared type plan).\n${decls}` : undefined,
|
|
1561
2016
|
`// Per-component CONCRETE input structs (fields = inputPorts; the consumer builds them natively —\n// no generic *Obj crosses the covered read boundary).\n${inStructs.join("\n\n")}`,
|
|
1562
|
-
|
|
2017
|
+
rowStructs.some((r) => r.trim().length > 0)
|
|
2018
|
+
? `// CONCRETE per-node handler-result row structs (typed fields = the node's outType; + error signal).\n${rowStructs.filter((r) => r.trim().length > 0).join("\n\n")}`
|
|
2019
|
+
: undefined,
|
|
1563
2020
|
`// Per-component CONCRETE handler interfaces (one typed Node_* method per covered node).\n${handlerIfaces.join("\n\n")}`,
|
|
1564
2021
|
elemCopiers.length
|
|
1565
2022
|
? `// Augmented map-element copiers (over element fields + the concrete into row — pure assignment).\n${elemCopiers.join("\n\n")}`
|
|
1566
2023
|
: undefined,
|
|
1567
|
-
|
|
2024
|
+
structs.some((r) => r.trim().length > 0)
|
|
2025
|
+
? `// Native ports structs (one per componentRef node; typed per the static port type).\n${structs.filter((r) => r.trim().length > 0).join("\n\n")}`
|
|
2026
|
+
: undefined,
|
|
2027
|
+
goExprUsed.helpers
|
|
2028
|
+
? `// #108 native-expr helper library (runtime-free; byte-equal to expr-eval.ts core) — used by\n// cond / map-guard checked arithmetic. NO bc-runtime reference (failures use the LOCAL *BehaviorError).\n${emitExprHelpers()}`
|
|
2029
|
+
: undefined,
|
|
1568
2030
|
`// Combined read runners (STRUCT-returning — the fully de-plumbed path).\n${runnerCodes.join("\n\n")}`,
|
|
1569
2031
|
structSurface,
|
|
1570
2032
|
].filter((s) => !!s);
|
|
@@ -1595,12 +2057,12 @@ export function goTypedNativeObserve(ir, runtimeImport) {
|
|
|
1595
2057
|
// decode the generic *Obj input into the CONCRETE In_<comp> struct (test glue — ONE decode at the
|
|
1596
2058
|
// top, OUTSIDE the covered hot path; a REQUIRED declared input is present so an absent key is a
|
|
1597
2059
|
// fail-closed decode error surfaced here, never on the native runner path).
|
|
1598
|
-
return `\tif name == ${JSON.stringify(c.name)} {\n\t\tin, ierr := decode_${inStructName(c.name)}(input)\n\t\tif ierr != nil {\n\t\t\treturn nil, ierr\n\t\t}\n\t\ttv, err :=
|
|
2060
|
+
return `\tif name == ${JSON.stringify(c.name)} {\n\t\tin, ierr := decode_${inStructName(c.name)}(input)\n\t\tif ierr != nil {\n\t\t\treturn nil, ierr\n\t\t}\n\t\ttv, err := ${runnerName(c.name)}(handlers, in)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn ${ser}, nil\n\t}`;
|
|
1599
2061
|
})
|
|
1600
2062
|
.join("\n");
|
|
1601
2063
|
// input decoders (TEST glue): generic *Obj -> concrete In_<comp>. Scalar fields read the typed value
|
|
1602
2064
|
// (a `.(T)` here is on the SDK/wire Value in the OBSERVE companion, never in the covered module).
|
|
1603
|
-
const inDecoders = native.map((c) => emitInDecoder(c)).join("\n\n");
|
|
2065
|
+
const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
|
|
1604
2066
|
// The observe companion drives EVERY covered component, so its handler type param H must satisfy
|
|
1605
2067
|
// EVERY per-component concrete Handler_<comp> interface — a combined constraint embeds them all.
|
|
1606
2068
|
const combinedConstraint = `allNativeRawHandlers`;
|
|
@@ -1621,6 +2083,8 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
|
1621
2083
|
for (const n of c.body)
|
|
1622
2084
|
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1623
2085
|
for (const n of c.body) {
|
|
2086
|
+
if ("cond" in n)
|
|
2087
|
+
continue; // #108: cond has no handler method (pure Expression) — no scripted impl.
|
|
1624
2088
|
const ref = typedNodes.get(n.id);
|
|
1625
2089
|
if ("map" in n) {
|
|
1626
2090
|
const m = n.map;
|
|
@@ -1788,7 +2252,7 @@ ${constraintDecl}
|
|
|
1788
2252
|
${methodImpls.join("\n\n")}
|
|
1789
2253
|
|
|
1790
2254
|
// ObserveNativeRaw drives the struct-native runner and serializes to a canonical Value (test only).
|
|
1791
|
-
// GENERIC over the CONCRETE combined handler type H — so each enclosed
|
|
2255
|
+
// GENERIC over the CONCRETE combined handler type H — so each enclosed RunNativeRawStruct_* call is
|
|
1792
2256
|
// instantiated at the concrete handler type (direct, devirtualized per-node method calls), not at an
|
|
1793
2257
|
// interface. Instantiate with your concrete handler type to preserve the covered path's concreteness.
|
|
1794
2258
|
func ObserveNativeRaw[H ${combinedConstraint}](name string, handlers H, input *${PKG}.Obj) (${PKG}.Value, error) {
|