behavior-contracts 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +495 -75
- 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 +553 -103
- 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,13 @@ 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) {
|
|
106
123
|
if (staticStringArrElems(node) !== null)
|
|
107
124
|
return "[]string";
|
|
108
125
|
const num = numLiteralGoExpr(node);
|
|
109
126
|
if (num !== null)
|
|
110
127
|
return Number.isInteger(node) ? "int64" : "float64";
|
|
111
|
-
const ft = inferPortType(node, inputPorts);
|
|
128
|
+
const ft = inferPortType(node, inputPorts, asBinding);
|
|
112
129
|
if (ft === "Value") {
|
|
113
130
|
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
131
|
}
|
|
@@ -236,27 +253,35 @@ function isNativeRaw(comp) {
|
|
|
236
253
|
* element struct by copying the over element's typed fields + materializing the into field from the
|
|
237
254
|
* aligned batch raw — NO boxed Value on any plane.
|
|
238
255
|
*/
|
|
239
|
-
function coveredMapNode(n, bodyIds) {
|
|
256
|
+
function coveredMapNode(n, bodyIds, comp) {
|
|
240
257
|
if (n === null || typeof n !== "object" || !("map" in n))
|
|
241
258
|
return false;
|
|
242
259
|
const m = n.map;
|
|
243
|
-
// #86 pt2: batched map...into. #93 shape 2:
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
if (m.when !== undefined)
|
|
260
|
+
// #86 pt2: batched map...into. #93 shape 2/3: (non-)batched map...into / connection-collect. #108:
|
|
261
|
+
// a guarded map (`when`) IS covered when it has NO `into` (guard+into is heterogeneous — skipped
|
|
262
|
+
// elements lack the into field, cannot de-box into one []T; keep it fail-closed, matching the typed
|
|
263
|
+
// straightline reject). A guarded map over a native-typed element collection drops skipped elements.
|
|
264
|
+
if (m.when !== undefined && m.into !== undefined)
|
|
248
265
|
return false;
|
|
249
266
|
if (m.policy !== undefined && m.policy !== "fail")
|
|
250
267
|
return false;
|
|
251
268
|
if (m.relationKind !== undefined && m.relationKind !== "single" && m.relationKind !== "connection")
|
|
252
269
|
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.
|
|
270
|
+
// `over` element type must be STATICALLY resolvable: a ref to a PRIOR BODY NODE's typed arr field
|
|
271
|
+
// (mapOverElemType) OR a ref to an INPUT array port that carries `elemType` (#108 — the C3 "BC does
|
|
272
|
+
// not infer" rule: an input array WITHOUT elemType stays fail-closed / uncovered).
|
|
257
273
|
const overE = classifyExpr(m.over);
|
|
258
|
-
if (overE.kind !== "ref"
|
|
274
|
+
if (overE.kind !== "ref")
|
|
259
275
|
return false;
|
|
276
|
+
const head = overE.path[0];
|
|
277
|
+
if (!bodyIds.has(head)) {
|
|
278
|
+
// input-array over: covered only when the input port declares elemType (#108).
|
|
279
|
+
if (overE.path.length !== 1)
|
|
280
|
+
return false;
|
|
281
|
+
const schema = comp.inputPorts?.[head];
|
|
282
|
+
if (schema === undefined || schema.elemType === undefined)
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
260
285
|
const ports = m.ports;
|
|
261
286
|
for (const p of Object.keys(ports))
|
|
262
287
|
if (!portIsStatic(ports[p]))
|
|
@@ -295,13 +320,21 @@ function coverageRejectReason(comp) {
|
|
|
295
320
|
}
|
|
296
321
|
const bodyIds = new Set(comp.body.map((b) => b.id));
|
|
297
322
|
for (const n of comp.body) {
|
|
298
|
-
if ("cond" in n)
|
|
299
|
-
|
|
323
|
+
if ("cond" in n) {
|
|
324
|
+
// #108: a cond node is native-covered — its `if`/`then`/`else` compile to native expressions via
|
|
325
|
+
// the runtime-free NativeExprCompiler (byte-equal to run_behavior's cond lower). If an expression
|
|
326
|
+
// is not statically compilable (uncovered op / refOpt / opt operand / type mismatch), the compiler
|
|
327
|
+
// fails closed at emit (a loud GeneratorFailure) — never a silent box. cond requires outType.
|
|
328
|
+
if (n.outType === undefined)
|
|
329
|
+
return `node '${n.id}' cond has no outType (required to de-box the branch join)`;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
300
332
|
if ("map" in n) {
|
|
301
|
-
// #86 pt2 batched map...into / #93 shape 2
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
333
|
+
// #86 pt2 batched map...into / #93 shape 2/3 / #108 guarded map (no into) / map-over-input (with
|
|
334
|
+
// elemType) ARE covered. A non-covered map shape (guard+into heterogeneous / over-input without
|
|
335
|
+
// elemType) falls through to the loud fail-closed reject.
|
|
336
|
+
if (!coveredMapNode(n, bodyIds, comp))
|
|
337
|
+
return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
|
|
305
338
|
continue;
|
|
306
339
|
}
|
|
307
340
|
if (!("component" in n))
|
|
@@ -342,13 +375,13 @@ function assertTypedCoverage(comp) {
|
|
|
342
375
|
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
376
|
}
|
|
344
377
|
// ── native ports struct + PortReader ──────────────────────────────────────────────────
|
|
345
|
-
function emitPortsStruct(comp, node) {
|
|
378
|
+
function emitPortsStruct(comp, node, asBinding) {
|
|
346
379
|
const structName = portsStructName(comp.name, node.id);
|
|
347
380
|
const portNames = Object.keys(node.ports);
|
|
348
381
|
if (portNames.length === 0) {
|
|
349
382
|
return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
|
|
350
383
|
}
|
|
351
|
-
const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp.inputPorts));
|
|
384
|
+
const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding));
|
|
352
385
|
const fieldNames = portNames.map(goPortFieldName);
|
|
353
386
|
const fields = portNames
|
|
354
387
|
.map((p, i) => `\t${fieldNames[i]} ${fieldTypes[i]} // ${JSON.stringify(p)}`)
|
|
@@ -391,6 +424,14 @@ function rawElemStructName(compName, nodeId) {
|
|
|
391
424
|
function handlerIfaceName(compName) {
|
|
392
425
|
return `Handler_${sanitize(compName)}`;
|
|
393
426
|
}
|
|
427
|
+
/** RunNativeRawStruct_<comp> — the EXPORTED name of the per-component struct-returning combined runner
|
|
428
|
+
* (bc#102). It MUST be an exported Go identifier so a consumer in a DIFFERENT package can call it (a
|
|
429
|
+
* leading-lowercase name is unexported / uncallable cross-package). The `RunNativeRawStruct_` prefix
|
|
430
|
+
* guarantees the leading char is uppercase regardless of how the component name sanitizes, and the
|
|
431
|
+
* sanitized component suffix keeps it unique per covered component. */
|
|
432
|
+
function runnerName(compName) {
|
|
433
|
+
return `RunNativeRawStruct_${sanitize(compName)}`;
|
|
434
|
+
}
|
|
394
435
|
/** Node_<comp>_<nodeId> — the concrete per-node handler method name. */
|
|
395
436
|
function nodeMethodName(compName, nodeId) {
|
|
396
437
|
return `Node_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
@@ -404,6 +445,9 @@ function nodeMethodName(compName, nodeId) {
|
|
|
404
445
|
function emitRawRowStructs(comp, typedNodes, plan) {
|
|
405
446
|
const parts = [];
|
|
406
447
|
for (const n of comp.body) {
|
|
448
|
+
// #108: a cond node has NO handler-result row (pure Expression — no dispatch).
|
|
449
|
+
if ("cond" in n)
|
|
450
|
+
continue;
|
|
407
451
|
const ref = typedNodes.get(n.id);
|
|
408
452
|
if ("map" in n) {
|
|
409
453
|
// a covered map node: RawElem_<comp>_<node> is the HANDLER'S per-element return shape:
|
|
@@ -498,6 +542,8 @@ function emitHandlerInterface(comp, typedNodes, plan) {
|
|
|
498
542
|
lines.push(`// Node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
|
|
499
543
|
lines.push(`type ${handlerIfaceName(comp.name)} interface {`);
|
|
500
544
|
for (const n of comp.body) {
|
|
545
|
+
if ("cond" in n)
|
|
546
|
+
continue; // #108: a cond node has no handler method (pure Expression).
|
|
501
547
|
const method = nodeMethodName(comp.name, n.id);
|
|
502
548
|
const bt = boundGoType(n, comp, typedNodes, plan);
|
|
503
549
|
if ("map" in n) {
|
|
@@ -524,10 +570,15 @@ function mapElemMaterializerName(compName, nodeId) {
|
|
|
524
570
|
* slice of the concrete per-element ports structs (NO `[]dslcontracts.Value`, NO PortReader accessor);
|
|
525
571
|
* the consumer reads `bp.Items[i].F_PK` directly.
|
|
526
572
|
*/
|
|
527
|
-
function emitMapPortsStructs(comp, node) {
|
|
573
|
+
function emitMapPortsStructs(comp, node, typedNodes, plan) {
|
|
528
574
|
const m = node.map;
|
|
529
575
|
const structName = portsStructName(comp.name, node.id);
|
|
530
|
-
|
|
576
|
+
// #108: the element ports may read the `$as` binding ($id / $g.field) — resolve the over element
|
|
577
|
+
// type so those ports type their struct field natively (map-over-input scalar element / prior-node
|
|
578
|
+
// struct element). A pure-static/concat port ignores the binding.
|
|
579
|
+
const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
580
|
+
const asBinding = { name: m.as, ref: elemRef, plan };
|
|
581
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding);
|
|
531
582
|
const batchStruct = `${structName}_batch`;
|
|
532
583
|
return `${elemStruct}
|
|
533
584
|
|
|
@@ -596,19 +647,35 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
|
596
647
|
}
|
|
597
648
|
return parts.join("\n\n");
|
|
598
649
|
}
|
|
599
|
-
/**
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
|
|
650
|
+
/**
|
|
651
|
+
* mapOverElemInfo — resolve a covered map's `over` collection: its element TypeRef + the Go expression
|
|
652
|
+
* that yields the native over slice. Two sources (#108):
|
|
653
|
+
* - a ref to a PRIOR BODY NODE's typed arr field (`t_root.Groups`) — element type from the parent struct;
|
|
654
|
+
* - a ref to an INPUT ARRAY port carrying `elemType` (`in.Ids`) — element type from PortSchema.elemType.
|
|
655
|
+
* A map over an input array WITHOUT elemType is fail-closed (BC does not infer — consumer-interface C3).
|
|
656
|
+
*/
|
|
657
|
+
function mapOverElemInfo(comp, node, typedNodes, plan) {
|
|
603
658
|
const m = node.map;
|
|
604
659
|
const e = classifyExpr(m.over);
|
|
605
660
|
if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
|
|
606
661
|
const baseRef = typedNodes.get(e.path[0]);
|
|
607
|
-
const { ref } = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
|
|
662
|
+
const { expr, ref } = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
|
|
608
663
|
if (ref.kind === "arr")
|
|
609
|
-
return
|
|
664
|
+
return { elemRef: ref.elem, overExpr: expr };
|
|
665
|
+
}
|
|
666
|
+
// input-array over (#108): the head is an input port; its declared elemType is the element type.
|
|
667
|
+
if (e.kind === "ref" && e.path.length === 1) {
|
|
668
|
+
const schema = comp.inputPorts?.[e.path[0]];
|
|
669
|
+
const et = schema?.elemType;
|
|
670
|
+
if (et !== undefined) {
|
|
671
|
+
return { elemRef: deriveTypeRef(et, plan), overExpr: `in.${goFieldName(e.path[0])}` };
|
|
672
|
+
}
|
|
610
673
|
}
|
|
611
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined map (#
|
|
674
|
+
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).`);
|
|
675
|
+
}
|
|
676
|
+
/** The Go type of the over-element (back-compat shim). */
|
|
677
|
+
function mapOverElemType(comp, node, typedNodes, plan) {
|
|
678
|
+
return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
|
|
612
679
|
}
|
|
613
680
|
function classifyPort(node) {
|
|
614
681
|
if (staticArrElems(node) !== null)
|
|
@@ -663,8 +730,8 @@ function reconstructG(e) {
|
|
|
663
730
|
function inStructName(compName) {
|
|
664
731
|
return `In_${sanitize(compName)}`;
|
|
665
732
|
}
|
|
666
|
-
/** Go type for an input port's declared type (scalar → concrete;
|
|
667
|
-
function inputPortGoType(schema) {
|
|
733
|
+
/** Go type for an input port's declared type (scalar → concrete; array+elemType → []ElemT; else Value). */
|
|
734
|
+
function inputPortGoType(schema, plan) {
|
|
668
735
|
switch (schema?.type) {
|
|
669
736
|
case "string":
|
|
670
737
|
return "string";
|
|
@@ -674,6 +741,15 @@ function inputPortGoType(schema) {
|
|
|
674
741
|
return "float64";
|
|
675
742
|
case "bool":
|
|
676
743
|
return "bool";
|
|
744
|
+
case "array":
|
|
745
|
+
case "arr": {
|
|
746
|
+
// #108: an input ARRAY port with a declared elemType lowers to a native []ElemT (the map-over-input
|
|
747
|
+
// $as element type source). Without elemType or a plan, it stays a boxed Value (uncovered / fail-closed).
|
|
748
|
+
const et = schema.elemType;
|
|
749
|
+
if (et !== undefined && plan !== undefined)
|
|
750
|
+
return `[]${renderTypeRef(deriveTypeRef(et, plan))}`;
|
|
751
|
+
return `${PKG}.Value`;
|
|
752
|
+
}
|
|
677
753
|
default:
|
|
678
754
|
return `${PKG}.Value`;
|
|
679
755
|
}
|
|
@@ -695,14 +771,14 @@ function inputScalarKind(schema) {
|
|
|
695
771
|
}
|
|
696
772
|
/** emitInStruct — the concrete input struct declaration for a covered component (fields = inputPorts,
|
|
697
773
|
* declaration order = Object key order; gofmt-aligned). Empty inputPorts => empty struct. */
|
|
698
|
-
function emitInStruct(comp) {
|
|
774
|
+
function emitInStruct(comp, plan) {
|
|
699
775
|
const name = inStructName(comp.name);
|
|
700
776
|
const ports = Object.keys(comp.inputPorts ?? {});
|
|
701
777
|
if (ports.length === 0) {
|
|
702
778
|
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
|
|
703
779
|
}
|
|
704
780
|
const names = ports.map((p) => goFieldName(p));
|
|
705
|
-
const types = ports.map((p) => inputPortGoType(comp.inputPorts[p]));
|
|
781
|
+
const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan));
|
|
706
782
|
const nameW = Math.max(0, ...names.map((n) => n.length));
|
|
707
783
|
const typeW = Math.max(0, ...types.map((t) => t.length));
|
|
708
784
|
const fields = ports
|
|
@@ -722,7 +798,7 @@ ${fields}
|
|
|
722
798
|
* pre-built by the consumer. A missing key leaves the zero value (every covered input is required +
|
|
723
799
|
* always supplied by the corpus); a present-but-wrong-scalar is a fail-closed decode error.
|
|
724
800
|
*/
|
|
725
|
-
function emitInDecoder(comp) {
|
|
801
|
+
function emitInDecoder(comp, plan) {
|
|
726
802
|
const name = inStructName(comp.name);
|
|
727
803
|
const ports = Object.keys(comp.inputPorts ?? {});
|
|
728
804
|
const lines = [];
|
|
@@ -739,8 +815,28 @@ function emitInDecoder(comp) {
|
|
|
739
815
|
lines.push(`\t}`);
|
|
740
816
|
for (const p of ports) {
|
|
741
817
|
const field = goFieldName(p);
|
|
742
|
-
const
|
|
818
|
+
const schema = comp.inputPorts[p];
|
|
819
|
+
const scalar = inputScalarKind(schema);
|
|
820
|
+
const et = schema.elemType;
|
|
743
821
|
lines.push(`\tif v, ok := input.Get(${JSON.stringify(p)}); ok {`);
|
|
822
|
+
if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
823
|
+
// #108: decode the boxed Value array into the native []ElemT (test glue — materialize each element
|
|
824
|
+
// via the typed materializer; a non-array or non-conforming element is a fail-closed decode error).
|
|
825
|
+
const elemRef = deriveTypeRef(et, plan);
|
|
826
|
+
const elemGo = renderTypeRef(elemRef);
|
|
827
|
+
lines.push(`\t\tarr, isArr := v.([]${PKG}.Value)`);
|
|
828
|
+
lines.push(`\t\tif !isArr {`);
|
|
829
|
+
lines.push(`\t\t\treturn in, ${PKG}.NewExprFailure("TYPE_MISMATCH", "input ${p}: expected array, got "+${PKG}.TypeName(v))`);
|
|
830
|
+
lines.push(`\t\t}`);
|
|
831
|
+
lines.push(`\t\tdec := make([]${elemGo}, 0, len(arr))`);
|
|
832
|
+
lines.push(`\t\tfor _, ev := range arr {`);
|
|
833
|
+
// materializeExpr returns the must*-based decode (panics on a malformed corpus element — test glue).
|
|
834
|
+
lines.push(`\t\t\tdec = append(dec, ${goTypedInternals.materializeExpr("ev", elemRef, plan)})`);
|
|
835
|
+
lines.push(`\t\t}`);
|
|
836
|
+
lines.push(`\t\tin.${field} = dec`);
|
|
837
|
+
lines.push(`\t}`);
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
744
840
|
if (scalar === undefined) {
|
|
745
841
|
lines.push(`\t\tin.${field} = v`);
|
|
746
842
|
}
|
|
@@ -985,7 +1081,7 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
985
1081
|
const outStructType = outRef ? renderTypeRef(outRef) : `${PKG}.Value`;
|
|
986
1082
|
const zeroOut = outRef ? goZero(outRef) : "nil";
|
|
987
1083
|
const lines = [];
|
|
988
|
-
lines.push(`//
|
|
1084
|
+
lines.push(`// ${runnerName(comp.name)} — the STRUCT-RETURNING combined read (bc#77/#87): the fully`);
|
|
989
1085
|
lines.push(`// de-plumbed path. Ports are a native struct (direct field assignment); the handler`);
|
|
990
1086
|
lines.push(`// result is materialized straight into the node's outType struct. Node results are typed`);
|
|
991
1087
|
lines.push(`// struct locals; a relation child reads the parent's REAL struct result via direct field`);
|
|
@@ -1001,7 +1097,7 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1001
1097
|
// whose Node_* methods return the concrete per-node row struct (typed fields = the node's outType).
|
|
1002
1098
|
// The covered result plane carries no boxed ports / boxed value / dynamic accessor / semantic type-
|
|
1003
1099
|
// assert on a row value — the runner reads row.<Field> directly and copies each into the outType local.
|
|
1004
|
-
lines.push(`func
|
|
1100
|
+
lines.push(`func ${runnerName(comp.name)}[H ${handlerIfaceName(comp.name)}](handlers H, in ${inStructName(comp.name)}) (${outStructType}, error) {`);
|
|
1005
1101
|
lines.push(`\t_ = in`);
|
|
1006
1102
|
// resolveNativeRef for this runner: a ref head is either a PRIOR NODE (read its typed struct field
|
|
1007
1103
|
// directly) or an INPUT PORT (read the typed `in.<Field>`). A REQUIRED scalar resolves to a native
|
|
@@ -1057,6 +1153,13 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1057
1153
|
lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut, flags));
|
|
1058
1154
|
continue;
|
|
1059
1155
|
}
|
|
1156
|
+
// #108: a cond node — pure Expression (no handler). Compile `if` to a native bool + materialize the
|
|
1157
|
+
// taken branch to the node's outType, assigning the node's typed cell. Byte-equal to run_behavior's
|
|
1158
|
+
// cond lower ({cond:[if,then,else]}, strict-bool, only the taken branch evaluated).
|
|
1159
|
+
if ("cond" in comp.body[i]) {
|
|
1160
|
+
lines.push(emitCondArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut));
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1060
1163
|
const node = comp.body[i];
|
|
1061
1164
|
const meta = metas[i];
|
|
1062
1165
|
const s = sanitize(node.id);
|
|
@@ -1174,22 +1277,37 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1174
1277
|
const m = node.map;
|
|
1175
1278
|
const batched = m.batched === true;
|
|
1176
1279
|
const hasInto = m.into !== undefined;
|
|
1280
|
+
const hasGuard = m.when !== undefined;
|
|
1177
1281
|
const s = sanitize(node.id);
|
|
1178
1282
|
const structName = portsStructName(comp.name, node.id);
|
|
1179
1283
|
const batchStruct = `${structName}_batch`;
|
|
1180
1284
|
const arrRef = typedNodes.get(node.id);
|
|
1181
1285
|
const elemGo = renderTypeRef(arrRef);
|
|
1182
1286
|
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;
|
|
1287
|
+
// over collection element type + the native over-slice expression (#108: prior-node arr OR input array
|
|
1288
|
+
// with elemType). The $as element is a value of `overElemRef` (input-array element or prior-node elem).
|
|
1289
|
+
const { elemRef: overElemRef, overExpr } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
1290
|
+
const overElemGo = renderTypeRef(overElemRef);
|
|
1190
1291
|
const elemMat = mapElemMaterializerName(comp.name, node.id);
|
|
1191
1292
|
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1192
1293
|
const portNames = Object.keys(m.ports);
|
|
1294
|
+
// #108 guard: compile the `when` expression to a native bool over an element-scoped resolveHead
|
|
1295
|
+
// ($as → the over element value `elemVar`; prior node → its typed local; input port → in.<Field>).
|
|
1296
|
+
// Returns the compiled {stmts, expr} or null (no guard). Mirrors run_behavior's per-element keep-gate
|
|
1297
|
+
// ({cond:[when,true,false]}, strict-bool). A non-bool / uncovered guard expr fails closed at emit.
|
|
1298
|
+
const mkGuard = (elemVar) => {
|
|
1299
|
+
if (!hasGuard)
|
|
1300
|
+
return null;
|
|
1301
|
+
const resolveHead = (head) => {
|
|
1302
|
+
if (head === asName)
|
|
1303
|
+
return { expr: elemVar, ref: overElemRef };
|
|
1304
|
+
if (priorHere(head))
|
|
1305
|
+
return { expr: typedLocal(head), ref: typedNodes.get(head) };
|
|
1306
|
+
return goInputHeadRef(head, comp, plan);
|
|
1307
|
+
};
|
|
1308
|
+
const be = makeGoExprBackend(zeroOut, resolveHead, plan);
|
|
1309
|
+
return new NativeExprCompiler(be).compileBool(m.when);
|
|
1310
|
+
};
|
|
1193
1311
|
// emit the per-element native ports struct build (shared; `il` is the indent inside the element
|
|
1194
1312
|
// loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
|
|
1195
1313
|
const buildPorts = (il, elemVar) => {
|
|
@@ -1233,8 +1351,9 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1233
1351
|
return null;
|
|
1234
1352
|
return { expr, scalar: goScalar(acc.scalar) };
|
|
1235
1353
|
};
|
|
1354
|
+
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1236
1355
|
for (const pn of portNames) {
|
|
1237
|
-
const fieldGoType = portFieldGoType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'
|
|
1356
|
+
const fieldGoType = portFieldGoType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding);
|
|
1238
1357
|
inits.push(emitPortInit(pn, m.ports[pn], fieldGoType, resolveRef));
|
|
1239
1358
|
}
|
|
1240
1359
|
out.push(`${il}ep_${s} := ${structName}{${inits.join(", ")}}`);
|
|
@@ -1254,41 +1373,69 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1254
1373
|
lines.push(`${indent}over_${s} := ${overExpr}`);
|
|
1255
1374
|
lines.push(`${indent}${typedLocal(node.id)} = make(${elemGo}, 0, len(over_${s}))`);
|
|
1256
1375
|
const elemRowRef = mapElemRowRef(node, arrRef, plan);
|
|
1376
|
+
// emit the per-element guard keep-gate (#108). `il` = indent, `elemVar` = the over element loop var.
|
|
1377
|
+
// On skip: batched → `continue` (element excluded from items + keptOver); non-batched → `continue`.
|
|
1378
|
+
const emitGuard = (il, elemVar) => {
|
|
1379
|
+
const g = mkGuard(elemVar);
|
|
1380
|
+
if (g === null)
|
|
1381
|
+
return [];
|
|
1382
|
+
const out = [];
|
|
1383
|
+
for (const st of g.stmts)
|
|
1384
|
+
out.push(`${il}${st}`);
|
|
1385
|
+
out.push(`${il}if !(${g.expr}) {`);
|
|
1386
|
+
out.push(`${il}\tcontinue`);
|
|
1387
|
+
out.push(`${il}}`);
|
|
1388
|
+
return out;
|
|
1389
|
+
};
|
|
1257
1390
|
if (batched) {
|
|
1258
|
-
// BATCHED (#86 pt2 into / #93 shape 3 no-into): build
|
|
1259
|
-
// Node_* returns
|
|
1391
|
+
// BATCHED (#86 pt2 into / #93 shape 3 no-into / #108 guarded no-into): build kept-element ports,
|
|
1392
|
+
// dispatch ONCE. The concrete Node_* returns RawRow{IsError,Err,Rows []RawElem} aligned to items.
|
|
1393
|
+
// With a guard, `items` (and, for into, `keptOver`) hold only kept elements — matching run_behavior's
|
|
1394
|
+
// keptIdx-aligned batch. `into` is never combined with a guard (rejected). no-into collects each row.
|
|
1260
1395
|
lines.push(`${indent}items_${s} := make([]${structName}, 0, len(over_${s}))`);
|
|
1396
|
+
if (hasInto)
|
|
1397
|
+
lines.push(`${indent}keptOver_${s} := make([]${overElemGo}, 0, len(over_${s}))`);
|
|
1261
1398
|
lines.push(`${indent}for _, ${elemLocal(s)} := range over_${s} {`);
|
|
1399
|
+
for (const l of emitGuard(indent + "\t", elemLocal(s)))
|
|
1400
|
+
lines.push(l);
|
|
1262
1401
|
for (const l of buildPorts(indent + "\t", elemLocal(s)))
|
|
1263
1402
|
lines.push(l);
|
|
1264
1403
|
lines.push(`${indent}\titems_${s} = append(items_${s}, ep_${s})`);
|
|
1404
|
+
if (hasInto)
|
|
1405
|
+
lines.push(`${indent}\tkeptOver_${s} = append(keptOver_${s}, ${elemLocal(s)})`);
|
|
1265
1406
|
lines.push(`${indent}}`);
|
|
1266
|
-
// empty
|
|
1407
|
+
// empty items => empty result (handler not called), matching run_behavior.
|
|
1267
1408
|
lines.push(`${indent}if len(items_${s}) > 0 {`);
|
|
1268
1409
|
lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: items_${s}}`);
|
|
1269
1410
|
lines.push(`${indent}\tmo_${s}, mo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
|
|
1270
1411
|
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
1412
|
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
1413
|
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
1414
|
if (hasInto) {
|
|
1275
|
-
// #86 pt2: zip-attach the into field onto each over element (augmented element copier).
|
|
1276
|
-
lines.push(`${indent}\
|
|
1415
|
+
// #86 pt2: zip-attach the into field onto each kept over element (augmented element copier).
|
|
1416
|
+
lines.push(`${indent}\tfor mk_${s} := range keptOver_${s} {`);
|
|
1417
|
+
lines.push(`${indent}\t\tel_${s} := ${elemMat}(keptOver_${s}[mk_${s}], mo_${s}.Rows[mk_${s}])`);
|
|
1418
|
+
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1419
|
+
lines.push(`${indent}\t}`);
|
|
1277
1420
|
}
|
|
1278
1421
|
else {
|
|
1279
|
-
// #93 shape 3: collect each element DIRECTLY from the
|
|
1422
|
+
// #93 shape 3 / #108: collect each element DIRECTLY from the aligned per-element row (field copy).
|
|
1423
|
+
lines.push(`${indent}\tfor mk_${s} := range mo_${s}.Rows {`);
|
|
1280
1424
|
lines.push(`${indent}\t\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
|
|
1281
1425
|
lines.push(emitRowCopy(`el_${s}`, `mo_${s}.Rows[mk_${s}]`, elemRowRef, plan, indent.length + 2));
|
|
1426
|
+
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1427
|
+
lines.push(`${indent}\t}`);
|
|
1282
1428
|
}
|
|
1283
|
-
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1284
|
-
lines.push(`${indent}\t}`);
|
|
1285
1429
|
lines.push(`${indent}}`);
|
|
1286
1430
|
}
|
|
1287
1431
|
else {
|
|
1288
|
-
// #93 shape 2: NON-batched map
|
|
1289
|
-
// requests, matching run_behavior's per-element loop).
|
|
1432
|
+
// #93 shape 2 / #108: NON-batched map — dispatch the child handler ONCE PER KEPT ELEMENT (N physical
|
|
1433
|
+
// requests, matching run_behavior's per-element loop). A guarded element that fails `when` is skipped
|
|
1434
|
+
// (no dispatch, excluded from the result). The concrete Node_* returns ONE RawElem_.
|
|
1290
1435
|
lines.push(`${indent}for mk_${s} := range over_${s} {`);
|
|
1291
1436
|
lines.push(`${indent}\t${elemLocal(s)} := over_${s}[mk_${s}]`);
|
|
1437
|
+
for (const l of emitGuard(indent + "\t", elemLocal(s)))
|
|
1438
|
+
lines.push(l);
|
|
1292
1439
|
for (const l of buildPorts(indent + "\t", elemLocal(s)))
|
|
1293
1440
|
lines.push(l);
|
|
1294
1441
|
// per-element dispatch (ONE physical request per element). run_behavior passes the over element
|
|
@@ -1317,6 +1464,76 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1317
1464
|
function elemLocal(s) {
|
|
1318
1465
|
return `oel_${s}`;
|
|
1319
1466
|
}
|
|
1467
|
+
/** #108: resolve an INPUT port head for a native-expr ref (cond `if` / map `when` / branches). A scalar
|
|
1468
|
+
* port → `in.<Field>` of the scalar type; an array port WITH elemType → `in.<Field>` of `[]ElemT` (so
|
|
1469
|
+
* `len(in.<Field>)` works); anything else → null (fail-closed). */
|
|
1470
|
+
function goInputHeadRef(head, comp, plan) {
|
|
1471
|
+
const schema = comp.inputPorts?.[head];
|
|
1472
|
+
if (schema === undefined)
|
|
1473
|
+
return null;
|
|
1474
|
+
const sc = inputScalarKind(schema);
|
|
1475
|
+
if (sc !== undefined) {
|
|
1476
|
+
const scalarRef = { kind: "scalar", scalar: sc === "int64" ? "int" : sc === "float64" ? "float" : sc };
|
|
1477
|
+
return { expr: `in.${goFieldName(head)}`, ref: scalarRef };
|
|
1478
|
+
}
|
|
1479
|
+
const et = schema.elemType;
|
|
1480
|
+
if ((schema.type === "array" || schema.type === "arr") && et !== undefined) {
|
|
1481
|
+
return { expr: `in.${goFieldName(head)}`, ref: { kind: "arr", elem: deriveTypeRef(et, plan) } };
|
|
1482
|
+
}
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* emitCondArm — the struct-native exec of a covered cond node (#108). Mirrors run_behavior's cond
|
|
1487
|
+
* lower (behavior.ts:425-429): compile `if` to a native bool, materialize the TAKEN branch to the
|
|
1488
|
+
* node's outType (only the taken branch evaluated — short-circuit), assign the node's typed cell and
|
|
1489
|
+
* set produced. NO handler dispatch (cond is a pure Expression). The `if`/`then`/`else` expressions
|
|
1490
|
+
* compile via the runtime-free NativeExprCompiler (byte-equal to `evaluate`); an uncovered op / opt /
|
|
1491
|
+
* refOpt / type-mismatch fails closed at emit.
|
|
1492
|
+
*/
|
|
1493
|
+
function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut) {
|
|
1494
|
+
const c = node.cond;
|
|
1495
|
+
const s = sanitize(node.id);
|
|
1496
|
+
const nodeRef = typedNodes.get(node.id);
|
|
1497
|
+
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1498
|
+
const resolveHead = (head) => {
|
|
1499
|
+
if (priorHere(head))
|
|
1500
|
+
return { expr: typedLocal(head), ref: typedNodes.get(head) };
|
|
1501
|
+
return goInputHeadRef(head, comp, plan);
|
|
1502
|
+
};
|
|
1503
|
+
const be = makeGoExprBackend(zeroOut, resolveHead, plan);
|
|
1504
|
+
const compiler = new NativeExprCompiler(be);
|
|
1505
|
+
const resolveDecl = (name) => findDecl(plan, name).fields;
|
|
1506
|
+
const cond = compiler.compileBool(c.if);
|
|
1507
|
+
const thenC = compiler.compileTo(c.then, nodeRef, resolveDecl);
|
|
1508
|
+
const elseC = compiler.compileTo(c.else, nodeRef, resolveDecl);
|
|
1509
|
+
const lines = [];
|
|
1510
|
+
lines.push(`\t// ── cond '${node.id}'${c.parent ? " (parent:" + c.parent + ")" : ""} — pure Expression (no handler) ──`);
|
|
1511
|
+
let indent = "\t";
|
|
1512
|
+
const closers = [];
|
|
1513
|
+
if (c.parent !== undefined && priorNodeAt(c.parent, atPos)) {
|
|
1514
|
+
lines.push(`${indent}if produced_${sanitize(c.parent)} {`);
|
|
1515
|
+
closers.unshift(`${indent}}`);
|
|
1516
|
+
indent += "\t";
|
|
1517
|
+
}
|
|
1518
|
+
// the `if` condition (its fallible temps hoisted before the branch — the condition is always evaluated).
|
|
1519
|
+
for (const st of cond.stmts)
|
|
1520
|
+
lines.push(`${indent}${st}`);
|
|
1521
|
+
lines.push(`${indent}if ${cond.expr} {`);
|
|
1522
|
+
// TAKEN = then: only this branch's stmts run (short-circuit ≡ evaluate).
|
|
1523
|
+
for (const st of thenC.stmts)
|
|
1524
|
+
lines.push(`${indent}\t${st}`);
|
|
1525
|
+
lines.push(`${indent}\t${typedLocal(node.id)} = ${thenC.expr}`);
|
|
1526
|
+
lines.push(`${indent}} else {`);
|
|
1527
|
+
for (const st of elseC.stmts)
|
|
1528
|
+
lines.push(`${indent}\t${st}`);
|
|
1529
|
+
lines.push(`${indent}\t${typedLocal(node.id)} = ${elseC.expr}`);
|
|
1530
|
+
lines.push(`${indent}}`);
|
|
1531
|
+
lines.push(`${indent}produced_${s} = true`);
|
|
1532
|
+
void nodeRef;
|
|
1533
|
+
for (const cl of closers)
|
|
1534
|
+
lines.push(cl);
|
|
1535
|
+
return lines.join("\n");
|
|
1536
|
+
}
|
|
1320
1537
|
/**
|
|
1321
1538
|
* emitProducedAwareValue — the #86 part 1 produced-aware output lowering (go). It mirrors the
|
|
1322
1539
|
* boxed-typed emitTypedValue, but a `ref`/`refOpt` whose HEAD is a skippable relationSingle child is
|
|
@@ -1433,9 +1650,190 @@ function goZero(ref) {
|
|
|
1433
1650
|
// bc#90 / runtime-free: the nv* helper set (nvVE/nvBindVE/nvFieldVE/nvCatVE/nvArrVE) is REMOVED —
|
|
1434
1651
|
// those were dslcontracts.Value-based; the covered plane now lowers every port to a native Go value
|
|
1435
1652
|
// (emitPortInit) so no VE helper is emitted, and the module imports ZERO bc runtime.
|
|
1653
|
+
// ── native expression compiler backend (#108) — cond `if` / map `when` / cond branches ──────────
|
|
1654
|
+
//
|
|
1655
|
+
// The runtime-free native expression compiler (native-expr.ts) mirrors runBehavior's cond/guard
|
|
1656
|
+
// Expression evaluation with BYTE-EQUAL semantics. The value-dependent runtime failures (INT_OVERFLOW
|
|
1657
|
+
// / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS / INVALID_LITERAL for a huge len) are emitted as calls to a
|
|
1658
|
+
// FIXED set of RUNTIME-FREE helper funcs baked into the module (bcExpr* below) — the covered module
|
|
1659
|
+
// still imports ZERO bc runtime. A fallible helper returns (T, *BehaviorError); the runner early-returns
|
|
1660
|
+
// the failure (its zero output value is `zeroOut`).
|
|
1661
|
+
let goExprTempSeq = 0;
|
|
1662
|
+
/** module-level accumulator: set true when the native-expr compiler emits a fallible helper call
|
|
1663
|
+
* (so emit() includes the helper library + imports `math`). Reset at the start of each emit(). */
|
|
1664
|
+
const goExprUsed = { helpers: false };
|
|
1665
|
+
/** GoNativeExprBackend — renders native-expr for Go. `zeroOut` is the runner's error-return zero
|
|
1666
|
+
* value; `resolveHead` maps a ref head (input port / input-array $as elem / prior node) to a typed
|
|
1667
|
+
* base expr. Sets goExprUsed.helpers when a fallible helper is emitted (so emit() bakes the helper lib). */
|
|
1668
|
+
function makeGoExprBackend(zeroOut, resolveHead, plan) {
|
|
1669
|
+
const used = goExprUsed;
|
|
1670
|
+
return {
|
|
1671
|
+
lang: "go",
|
|
1672
|
+
freshTemp: () => `ex_${goExprTempSeq++}`,
|
|
1673
|
+
renderType: (ref) => renderTypeRef(ref),
|
|
1674
|
+
scalarType: (s) => goScalar(s),
|
|
1675
|
+
strLit: (s) => JSON.stringify(s),
|
|
1676
|
+
boolLit: (b) => (b ? "true" : "false"),
|
|
1677
|
+
intLit: (dec) => `int64(${dec})`,
|
|
1678
|
+
floatLit: (n) => `float64(${goFloatLitExpr(n)})`,
|
|
1679
|
+
fieldAccess: (baseExpr, baseRef, path) => goTypedInternals.typedFieldAccess(baseExpr, baseRef, path, plan),
|
|
1680
|
+
resolveHead,
|
|
1681
|
+
hoistFallible: (temp, _ty, call) => {
|
|
1682
|
+
used.helpers = true;
|
|
1683
|
+
// a unique err var per hoist (Go `:=` re-declares fine in the same block only for NEW LHS; use a
|
|
1684
|
+
// fresh err name to avoid "no new variables on left side of :=" when several hoists share a block).
|
|
1685
|
+
const errv = `err_${goExprTempSeq++}`;
|
|
1686
|
+
return [`${temp}, ${errv} := ${call}`, `if ${errv} != nil {`, `\treturn ${zeroOut}, ${errv}`, `}`];
|
|
1687
|
+
},
|
|
1688
|
+
helperCall: (helper, args) => {
|
|
1689
|
+
used.helpers = true;
|
|
1690
|
+
return `bcExpr_${helper}(${args.join(", ")})`;
|
|
1691
|
+
},
|
|
1692
|
+
binOp: (op, a, b) => `(${a} ${op} ${b})`,
|
|
1693
|
+
concat: (parts) => {
|
|
1694
|
+
// native string + concat (all parts are native strings) — no import needed.
|
|
1695
|
+
return `(${parts.join(" + ")})`;
|
|
1696
|
+
},
|
|
1697
|
+
notOp: (a) => `(!${a})`,
|
|
1698
|
+
structLit: (name, inits) => `${name}{${inits.map((i) => `${goFieldName(i.field)}: ${i.expr}`).join(", ")}}`,
|
|
1699
|
+
arrLit: (elemTy, items) => `[]${elemTy}{${items.join(", ")}}`,
|
|
1700
|
+
ternary: (cond, t, e, ty) => `func() ${ty} { if ${cond} { return ${t} }; return ${e} }()`,
|
|
1701
|
+
shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
|
|
1702
|
+
// and: temp=false; if left { <rStmts>; temp=rExpr }. or: temp=true; if !left { <rStmts>; temp=rExpr }.
|
|
1703
|
+
const gate = op === "and" ? left : `!(${left})`;
|
|
1704
|
+
const seed = op === "and" ? "false" : "true";
|
|
1705
|
+
return [
|
|
1706
|
+
`var ${temp} bool = ${seed}`,
|
|
1707
|
+
`if ${gate} {`,
|
|
1708
|
+
...rStmts.map((s) => `\t${s}`),
|
|
1709
|
+
`\t${temp} = ${rExpr}`,
|
|
1710
|
+
`}`,
|
|
1711
|
+
];
|
|
1712
|
+
},
|
|
1713
|
+
condGuard: (temp, ty, cond, tStmts, tExpr, eStmts, eExpr) => [
|
|
1714
|
+
`var ${temp} ${ty}`,
|
|
1715
|
+
`if ${cond} {`,
|
|
1716
|
+
...tStmts.map((s) => `\t${s}`),
|
|
1717
|
+
`\t${temp} = ${tExpr}`,
|
|
1718
|
+
`} else {`,
|
|
1719
|
+
...eStmts.map((s) => `\t${s}`),
|
|
1720
|
+
`\t${temp} = ${eExpr}`,
|
|
1721
|
+
`}`,
|
|
1722
|
+
],
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
/** Go float literal (finite; matches goFloatLit convention used elsewhere). */
|
|
1726
|
+
function goFloatLitExpr(n) {
|
|
1727
|
+
if (Number.isInteger(n))
|
|
1728
|
+
return `${n}.0`;
|
|
1729
|
+
return String(n);
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* emitExprHelpers — the FIXED runtime-free native-expr helper library (Go). Emitted once per covered
|
|
1733
|
+
* module that uses any cond/guard/arith. Each helper is BYTE-EQUAL to the reference core in
|
|
1734
|
+
* expr-eval.ts (checkI64 / checkFinite / mod trunc / div widen / code-point compare / lenCore). All
|
|
1735
|
+
* failures are the LOCAL *BehaviorError with the SAME code strings as run_behavior.
|
|
1736
|
+
*/
|
|
1737
|
+
function emitExprHelpers() {
|
|
1738
|
+
return `// ── native-expr runtime-free helper library (#108) — byte-equal to expr-eval.ts core ──
|
|
1739
|
+
const bcI64Max int64 = 9223372036854775807
|
|
1740
|
+
const bcI64Min int64 = -9223372036854775808
|
|
1741
|
+
|
|
1742
|
+
func bcExpr_addI64(a, b int64) (int64, *BehaviorError) {
|
|
1743
|
+
r := a + b
|
|
1744
|
+
// signed overflow detection (a+b): same-sign operands, result flips sign.
|
|
1745
|
+
if (a > 0 && b > 0 && r < 0) || (a < 0 && b < 0 && r > 0) {
|
|
1746
|
+
return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
|
|
1747
|
+
}
|
|
1748
|
+
return r, nil
|
|
1749
|
+
}
|
|
1750
|
+
func bcExpr_subI64(a, b int64) (int64, *BehaviorError) {
|
|
1751
|
+
r := a - b
|
|
1752
|
+
if (a >= 0 && b < 0 && r < 0) || (a < 0 && b > 0 && r >= 0) {
|
|
1753
|
+
return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
|
|
1754
|
+
}
|
|
1755
|
+
return r, nil
|
|
1756
|
+
}
|
|
1757
|
+
func bcExpr_mulI64(a, b int64) (int64, *BehaviorError) {
|
|
1758
|
+
if a == 0 || b == 0 {
|
|
1759
|
+
return 0, nil
|
|
1760
|
+
}
|
|
1761
|
+
r := a * b
|
|
1762
|
+
if r/b != a || (a == bcI64Min && b == -1) {
|
|
1763
|
+
return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
|
|
1764
|
+
}
|
|
1765
|
+
return r, nil
|
|
1766
|
+
}
|
|
1767
|
+
func bcExpr_negI64(a int64) (int64, *BehaviorError) {
|
|
1768
|
+
if a == bcI64Min {
|
|
1769
|
+
return 0, &BehaviorError{Code: "INT_OVERFLOW", Message: "i64 overflow"}
|
|
1770
|
+
}
|
|
1771
|
+
return -a, nil
|
|
1772
|
+
}
|
|
1773
|
+
func bcExpr_checkFinite(v float64) (float64, *BehaviorError) {
|
|
1774
|
+
if v != v || v > 1.7976931348623157e308 || v < -1.7976931348623157e308 {
|
|
1775
|
+
return 0, &BehaviorError{Code: "NAN_OR_INF", Message: "non-finite float"}
|
|
1776
|
+
}
|
|
1777
|
+
return v, nil
|
|
1778
|
+
}
|
|
1779
|
+
func bcExpr_addF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a + b) }
|
|
1780
|
+
func bcExpr_subF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a - b) }
|
|
1781
|
+
func bcExpr_mulF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a * b) }
|
|
1782
|
+
func bcExpr_negF(a float64) (float64, *BehaviorError) { return bcExpr_checkFinite(-a) }
|
|
1783
|
+
func bcExpr_modF(a, b float64) (float64, *BehaviorError) { m := math.Mod(a, b); return bcExpr_checkFinite(m) }
|
|
1784
|
+
func bcExpr_modI64(a, b int64) (int64, *BehaviorError) {
|
|
1785
|
+
if b == 0 {
|
|
1786
|
+
return 0, &BehaviorError{Code: "MOD_ZERO", Message: "int mod by zero"}
|
|
1787
|
+
}
|
|
1788
|
+
// Go % is truncated (sign of dividend) — matches modCore.
|
|
1789
|
+
r := a % b
|
|
1790
|
+
// overflow only at MinInt64 % -1 → 0 in Go (no overflow); still checkI64-safe.
|
|
1791
|
+
return r, nil
|
|
1792
|
+
}
|
|
1793
|
+
func bcExpr_widenI64F(a int64) (float64, *BehaviorError) {
|
|
1794
|
+
// div widening exact range is ±2^53 (WIDEN_EXACT). Outside → PRECISION_LOSS.
|
|
1795
|
+
if a > 9007199254740992 || a < -9007199254740992 {
|
|
1796
|
+
return 0, &BehaviorError{Code: "PRECISION_LOSS", Message: "int exceeds exact float range (±2^53)"}
|
|
1797
|
+
}
|
|
1798
|
+
return float64(a), nil
|
|
1799
|
+
}
|
|
1800
|
+
func bcExpr_divF(a, b float64) (float64, *BehaviorError) { return bcExpr_checkFinite(a / b) }
|
|
1801
|
+
func bcExpr_lenArr[T any](a []T) (int64, *BehaviorError) {
|
|
1802
|
+
return int64(len(a)), nil
|
|
1803
|
+
}
|
|
1804
|
+
// code-point (UTF-8 rune) order comparison — matches cmpCodePoints (NOT UTF-16 / byte compare, which
|
|
1805
|
+
// Go's native string < already is for valid UTF-8, but we compare rune-by-rune to match exactly).
|
|
1806
|
+
func bcExpr_cmpCP(a, b string) int {
|
|
1807
|
+
ra := []rune(a)
|
|
1808
|
+
rb := []rune(b)
|
|
1809
|
+
n := len(ra)
|
|
1810
|
+
if len(rb) < n {
|
|
1811
|
+
n = len(rb)
|
|
1812
|
+
}
|
|
1813
|
+
for i := 0; i < n; i++ {
|
|
1814
|
+
if ra[i] != rb[i] {
|
|
1815
|
+
if ra[i] < rb[i] {
|
|
1816
|
+
return -1
|
|
1817
|
+
}
|
|
1818
|
+
return 1
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
if len(ra) == len(rb) {
|
|
1822
|
+
return 0
|
|
1823
|
+
}
|
|
1824
|
+
if len(ra) < len(rb) {
|
|
1825
|
+
return -1
|
|
1826
|
+
}
|
|
1827
|
+
return 1
|
|
1828
|
+
}
|
|
1829
|
+
func bcExpr_strLt(a, b string) bool { return bcExpr_cmpCP(a, b) < 0 }
|
|
1830
|
+
func bcExpr_strLe(a, b string) bool { return bcExpr_cmpCP(a, b) <= 0 }
|
|
1831
|
+
func bcExpr_strGt(a, b string) bool { return bcExpr_cmpCP(a, b) > 0 }
|
|
1832
|
+
func bcExpr_strGe(a, b string) bool { return bcExpr_cmpCP(a, b) >= 0 }`;
|
|
1833
|
+
}
|
|
1436
1834
|
// ── BindNativeRaw dispatch surface ──────────────────────────────────────────────────────
|
|
1437
1835
|
/** The struct dispatch surface: ComponentNamesNativeRaw + the per-component struct runners
|
|
1438
|
-
* (
|
|
1836
|
+
* (RunNativeRawStruct_<comp>) ARE the exposed API. The consumer calls the struct runner directly
|
|
1439
1837
|
* (it returns the typed model — NO Value serialization on the read hot path, matching how graphddb
|
|
1440
1838
|
* reads: a model, never a boxed Value). There is deliberately NO Value dispatch map — that would
|
|
1441
1839
|
* require a Value serializer (the boxing residue bc#77 eliminates). A canonical Value observation
|
|
@@ -1443,7 +1841,7 @@ function goZero(ref) {
|
|
|
1443
1841
|
* which lives OUTSIDE this module so the covered module stays literal whole-file zero. */
|
|
1444
1842
|
function emitStructSurface(native) {
|
|
1445
1843
|
return `// ComponentNamesNativeRaw — covered reads exposed on the combined struct-native path (declaration
|
|
1446
|
-
// order). Each is driven via
|
|
1844
|
+
// order). Each is driven via RunNativeRawStruct_<comp>(handlers, input) -> (T, error): a STRUCT
|
|
1447
1845
|
// return; the consumer keeps the model native (no Value serialization on the read hot path — the
|
|
1448
1846
|
// boxing residue bc#77 removes). See INTEGRATION.md §6.
|
|
1449
1847
|
var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name)).join(", ")}}`;
|
|
@@ -1453,11 +1851,13 @@ var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name
|
|
|
1453
1851
|
* mismatch Sprintf, `strings` for a concat builder, `sync` for a parallel stage). ExpectedSpecVersions
|
|
1454
1852
|
* stays as a local `var` for provenance, but the old `dslcontracts.SpecVersions` skew `init()` is GONE
|
|
1455
1853
|
* (change #5 — there is no runtime to skew against once the module is runtime-free). */
|
|
1456
|
-
function emitMinimalHeader(ctx, needStrings, needSync, needFmt) {
|
|
1854
|
+
function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath) {
|
|
1457
1855
|
const sv = ctx.specVersions;
|
|
1458
1856
|
const imports = [];
|
|
1459
1857
|
if (needFmt)
|
|
1460
1858
|
imports.push(`\t"fmt"`);
|
|
1859
|
+
if (needMath)
|
|
1860
|
+
imports.push(`\t"math"`);
|
|
1461
1861
|
if (needStrings)
|
|
1462
1862
|
imports.push(`\t"strings"`);
|
|
1463
1863
|
if (needSync)
|
|
@@ -1484,6 +1884,9 @@ function emit(ctx) {
|
|
|
1484
1884
|
const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
|
|
1485
1885
|
const plan = buildTypePlan(ctx.ir);
|
|
1486
1886
|
const decls = emitTypeDecls(plan);
|
|
1887
|
+
// #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler when it emits
|
|
1888
|
+
// a fallible checked-arith helper call — determines whether the helper library + `math` import ship).
|
|
1889
|
+
goExprUsed.helpers = false;
|
|
1487
1890
|
// Emit the runners first so the nv* helper usage flags are known.
|
|
1488
1891
|
const flags = { bind: false, field: false, cat: false, arr: false };
|
|
1489
1892
|
const structs = [];
|
|
@@ -1493,18 +1896,23 @@ function emit(ctx) {
|
|
|
1493
1896
|
const elemCopiers = [];
|
|
1494
1897
|
const runnerCodes = [];
|
|
1495
1898
|
for (const c of native) {
|
|
1496
|
-
inStructs.push(emitInStruct(c));
|
|
1899
|
+
inStructs.push(emitInStruct(c, plan));
|
|
1497
1900
|
const typedNodes = new Map();
|
|
1901
|
+
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
1902
|
+
for (const n of c.body)
|
|
1903
|
+
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1498
1904
|
for (const n of c.body) {
|
|
1499
1905
|
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));
|
|
1906
|
+
// #86 pt2 / #108: a map node emits an element ports struct + a batch struct. The element ports
|
|
1907
|
+
// may read the `$as` binding — resolved via the over-element type (prior node / input array).
|
|
1908
|
+
structs.push(emitMapPortsStructs(c, n, typedNodes, plan));
|
|
1909
|
+
}
|
|
1910
|
+
else if ("cond" in n) {
|
|
1911
|
+
// #108: a cond node has NO ports struct (it is a pure Expression — no handler dispatch).
|
|
1503
1912
|
}
|
|
1504
1913
|
else {
|
|
1505
1914
|
structs.push(emitPortsStruct(c, n));
|
|
1506
1915
|
}
|
|
1507
|
-
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1508
1916
|
}
|
|
1509
1917
|
// CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
|
|
1510
1918
|
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
@@ -1539,7 +1947,10 @@ function emit(ctx) {
|
|
|
1539
1947
|
`box uncovered shapes. Implement native coverage for this shape, fix the spec if the shape is ` +
|
|
1540
1948
|
`genuinely runtime-dynamic, or generate this component on the --ir dynamic interpreter surface.`);
|
|
1541
1949
|
}
|
|
1542
|
-
|
|
1950
|
+
// #108: `math` is imported iff the native-expr compiler emitted a fallible helper (bcExpr_modF uses
|
|
1951
|
+
// math.Mod). The helper library itself is baked into the module only when used (goExprUsed.helpers).
|
|
1952
|
+
const needMath = goExprUsed.helpers;
|
|
1953
|
+
const header = emitMinimalHeader(ctx, false, needSync, needFmt, needMath);
|
|
1543
1954
|
const banner = `// COMBINED READ layer (bc#90 — the RUNTIME-FREE read de-box; STRUCT-ONLY, FULLY-NATIVE surface). A
|
|
1544
1955
|
// covered read is a strictly-sequential typed componentRef chain (point reads + relationKind:single
|
|
1545
1956
|
// children), emitted ONLY here: each node builds a native ports struct by direct field assignment (every
|
|
@@ -1551,7 +1962,7 @@ function emit(ctx) {
|
|
|
1551
1962
|
// *BehaviorError (same codes, byte-equal to run_behavior). Exec is INLINE sequential (no plan driver) so
|
|
1552
1963
|
// a relation child reads the parent's REAL struct result via direct field access and the child-present
|
|
1553
1964
|
// 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
|
|
1965
|
+
// (fixes #323/#74). The exposed API is the STRUCT-returning RunNativeRawStruct_<comp> (the consumer
|
|
1555
1966
|
// keeps the model native — no serialization on the read hot path). This module is FULLY NATIVE: a naive
|
|
1556
1967
|
// grep for boxing primitives OR for the bc-runtime package finds nothing, by design (bc#90/runtime-free).`;
|
|
1557
1968
|
const sections = [
|
|
@@ -1559,12 +1970,19 @@ function emit(ctx) {
|
|
|
1559
1970
|
`// 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
1971
|
decls ? `// Typed struct declarations (outType-derived; hash-dedup — shared type plan).\n${decls}` : undefined,
|
|
1561
1972
|
`// 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
|
-
|
|
1973
|
+
rowStructs.some((r) => r.trim().length > 0)
|
|
1974
|
+
? `// 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")}`
|
|
1975
|
+
: undefined,
|
|
1563
1976
|
`// Per-component CONCRETE handler interfaces (one typed Node_* method per covered node).\n${handlerIfaces.join("\n\n")}`,
|
|
1564
1977
|
elemCopiers.length
|
|
1565
1978
|
? `// Augmented map-element copiers (over element fields + the concrete into row — pure assignment).\n${elemCopiers.join("\n\n")}`
|
|
1566
1979
|
: undefined,
|
|
1567
|
-
|
|
1980
|
+
structs.some((r) => r.trim().length > 0)
|
|
1981
|
+
? `// Native ports structs (one per componentRef node; typed per the static port type).\n${structs.filter((r) => r.trim().length > 0).join("\n\n")}`
|
|
1982
|
+
: undefined,
|
|
1983
|
+
goExprUsed.helpers
|
|
1984
|
+
? `// #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()}`
|
|
1985
|
+
: undefined,
|
|
1568
1986
|
`// Combined read runners (STRUCT-returning — the fully de-plumbed path).\n${runnerCodes.join("\n\n")}`,
|
|
1569
1987
|
structSurface,
|
|
1570
1988
|
].filter((s) => !!s);
|
|
@@ -1595,12 +2013,12 @@ export function goTypedNativeObserve(ir, runtimeImport) {
|
|
|
1595
2013
|
// decode the generic *Obj input into the CONCRETE In_<comp> struct (test glue — ONE decode at the
|
|
1596
2014
|
// top, OUTSIDE the covered hot path; a REQUIRED declared input is present so an absent key is a
|
|
1597
2015
|
// 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 :=
|
|
2016
|
+
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
2017
|
})
|
|
1600
2018
|
.join("\n");
|
|
1601
2019
|
// input decoders (TEST glue): generic *Obj -> concrete In_<comp>. Scalar fields read the typed value
|
|
1602
2020
|
// (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");
|
|
2021
|
+
const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
|
|
1604
2022
|
// The observe companion drives EVERY covered component, so its handler type param H must satisfy
|
|
1605
2023
|
// EVERY per-component concrete Handler_<comp> interface — a combined constraint embeds them all.
|
|
1606
2024
|
const combinedConstraint = `allNativeRawHandlers`;
|
|
@@ -1621,6 +2039,8 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
|
1621
2039
|
for (const n of c.body)
|
|
1622
2040
|
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1623
2041
|
for (const n of c.body) {
|
|
2042
|
+
if ("cond" in n)
|
|
2043
|
+
continue; // #108: cond has no handler method (pure Expression) — no scripted impl.
|
|
1624
2044
|
const ref = typedNodes.get(n.id);
|
|
1625
2045
|
if ("map" in n) {
|
|
1626
2046
|
const m = n.map;
|
|
@@ -1788,7 +2208,7 @@ ${constraintDecl}
|
|
|
1788
2208
|
${methodImpls.join("\n\n")}
|
|
1789
2209
|
|
|
1790
2210
|
// 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
|
|
2211
|
+
// GENERIC over the CONCRETE combined handler type H — so each enclosed RunNativeRawStruct_* call is
|
|
1792
2212
|
// instantiated at the concrete handler type (direct, devirtualized per-node method calls), not at an
|
|
1793
2213
|
// interface. Instantiate with your concrete handler type to preserve the covered path's concreteness.
|
|
1794
2214
|
func ObserveNativeRaw[H ${combinedConstraint}](name string, handlers H, input *${PKG}.Obj) (${PKG}.Value, error) {
|