behavior-contracts 0.8.3 → 0.8.5
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/authoring.d.ts +1 -1
- package/dist/authoring.d.ts.map +1 -1
- package/dist/authoring.js +41 -2
- package/dist/authoring.js.map +1 -1
- package/dist/behavior.d.ts +0 -24
- package/dist/behavior.d.ts.map +1 -1
- package/dist/behavior.js +24 -2
- package/dist/behavior.js.map +1 -1
- package/dist/generator/emit-shared-go-typed.d.ts.map +1 -1
- package/dist/generator/emit-shared-go-typed.js +7 -7
- package/dist/generator/emit-shared-go-typed.js.map +1 -1
- package/dist/generator/emit-shared-rust-typed.d.ts +8 -0
- package/dist/generator/emit-shared-rust-typed.d.ts.map +1 -1
- package/dist/generator/emit-shared-rust-typed.js +20 -6
- package/dist/generator/emit-shared-rust-typed.js.map +1 -1
- package/dist/generator/emit-shared-typescript.d.ts.map +1 -1
- package/dist/generator/emit-shared-typescript.js +18 -2
- package/dist/generator/emit-shared-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 +231 -606
- package/dist/generator/emit-typed-native-go.js.map +1 -1
- package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-rust.js +287 -648
- package/dist/generator/emit-typed-native-rust.js.map +1 -1
- package/dist/generator/native-expr.d.ts +91 -7
- package/dist/generator/native-expr.d.ts.map +1 -1
- package/dist/generator/native-expr.js +124 -19
- package/dist/generator/native-expr.js.map +1 -1
- package/dist/generator/typed.d.ts +26 -0
- package/dist/generator/typed.d.ts.map +1 -1
- package/dist/generator/typed.js +52 -0
- package/dist/generator/typed.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@ import { GeneratorFailure } from "./core.js";
|
|
|
2
2
|
import { isControlGate } from "../behavior.js";
|
|
3
3
|
import { goStraightlineInternals } from "./emit-shared-go.js";
|
|
4
4
|
import { goTypedInternals } from "./emit-shared-go-typed.js";
|
|
5
|
-
import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
|
|
5
|
+
import { buildTypePlan, deriveTypeRef, findDecl, inputPortIsOptional, inputPortTypeRef } from "./typed.js";
|
|
6
6
|
import { concurrencyPlan, execPlan, analyzeSequentialOps, classifyExpr, buildComponentPlan, } from "./straightline.js";
|
|
7
7
|
import { NativeExprCompiler } from "./native-expr.js";
|
|
8
8
|
const PKG = "dslcontracts";
|
|
@@ -49,161 +49,70 @@ function portsStructName(compName, nodeId) {
|
|
|
49
49
|
// type to implement the per-component Handler_<comp> interface's concrete Node_* method signatures.
|
|
50
50
|
return `PortsNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
51
51
|
}
|
|
52
|
-
function inferPortType(node, inputPorts, asBinding) {
|
|
53
|
-
if (staticArrElems(node) !== null)
|
|
54
|
-
return "Value";
|
|
55
|
-
const e = classifyExpr(node);
|
|
56
|
-
switch (e.kind) {
|
|
57
|
-
case "str":
|
|
58
|
-
case "concat":
|
|
59
|
-
return "string";
|
|
60
|
-
case "bool":
|
|
61
|
-
return "bool";
|
|
62
|
-
case "ref": {
|
|
63
|
-
// #108: a `$as`-headed ref (map element binding) — resolve the element field's scalar type.
|
|
64
|
-
if (asBinding !== undefined && e.path[0] === asBinding.name) {
|
|
65
|
-
try {
|
|
66
|
-
const { ref } = goTypedInternals.typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan);
|
|
67
|
-
if (ref.kind === "scalar" && ref.scalar !== "null")
|
|
68
|
-
return goScalarPortField(ref.scalar);
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
return "Value";
|
|
72
|
-
}
|
|
73
|
-
return "Value";
|
|
74
|
-
}
|
|
75
|
-
if (e.path.length === 1) {
|
|
76
|
-
const s = inputPorts[e.path[0]];
|
|
77
|
-
if (s) {
|
|
78
|
-
if (s.type === "string" || s.type === "literal")
|
|
79
|
-
return "string"; // literal = constrained string.
|
|
80
|
-
if (s.type === "int")
|
|
81
|
-
return "int64";
|
|
82
|
-
if (s.type === "float" || s.type === "number")
|
|
83
|
-
return "float64"; // `"number"` = the float channel.
|
|
84
|
-
if (s.type === "bool")
|
|
85
|
-
return "bool";
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return "Value";
|
|
89
|
-
}
|
|
90
|
-
default:
|
|
91
|
-
return "Value";
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
/** scalar TypeRef → PortFieldType (native go scalar kind). */
|
|
95
|
-
function goScalarPortField(scalar) {
|
|
96
|
-
return scalar === "string" ? "string" : scalar === "int" ? "int64" : scalar === "float" ? "float64" : "bool";
|
|
97
|
-
}
|
|
98
|
-
/** A static string-array port (every element a static string literal) — the graphddb `projection`
|
|
99
|
-
* shape. Returns the list of literal strings, or null when the node is not that shape. */
|
|
100
|
-
function staticStringArrElems(node) {
|
|
101
|
-
const arr = staticArrElems(node);
|
|
102
|
-
if (arr === null)
|
|
103
|
-
return null;
|
|
104
|
-
const out = [];
|
|
105
|
-
for (const el of arr) {
|
|
106
|
-
const e = classifyExpr(el);
|
|
107
|
-
if (e.kind !== "str")
|
|
108
|
-
return null;
|
|
109
|
-
out.push(e.value);
|
|
110
|
-
}
|
|
111
|
-
return out;
|
|
112
|
-
}
|
|
113
52
|
/**
|
|
114
|
-
* portFieldGoType — the NATIVE Go type for a covered port field (bc#90 / runtime-free)
|
|
115
|
-
* ports struct carries NO `dslcontracts.Value`
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
|
|
120
|
-
* Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no
|
|
121
|
-
* `dslcontracts.Value` fallback on the native module). `where` names the emission site for the error.
|
|
53
|
+
* portFieldGoType — the NATIVE Go type for a covered port field (bc#90 / runtime-free), read off the ONE
|
|
54
|
+
* port lowering (compilePortNode / native-expr). The covered ports struct carries NO `dslcontracts.Value`
|
|
55
|
+
* field: every port lowers to a concrete Go type, and a port that cannot lower FAILS CLOSED. The field
|
|
56
|
+
* type is the compiled expression's TypeRef, so it can never disagree with the initializer (emitPortInit),
|
|
57
|
+
* which is the SAME lowering read for its value. `where` names the emission site for the error.
|
|
122
58
|
*/
|
|
123
|
-
function portFieldGoType(node,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → []ElemT.
|
|
127
|
-
const arrElem = portArrayElemRef(node, inputPorts, plan);
|
|
128
|
-
if (arrElem !== null)
|
|
129
|
-
return `[]${renderTypeRef(arrElem)}`;
|
|
130
|
-
// #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) → []ElemT.
|
|
131
|
-
const priorArr = priorNodeArrElemInfo(node, typedNodes, plan);
|
|
132
|
-
if (priorArr !== null)
|
|
133
|
-
return `[]${renderTypeRef(priorArr.elemRef)}`;
|
|
134
|
-
const num = numLiteralGoExpr(node);
|
|
135
|
-
if (num !== null)
|
|
136
|
-
return Number.isInteger(node) ? "int64" : "float64";
|
|
137
|
-
// a ref to a `$as` element field (or an input map port) whose type is a MAP or NAMED struct (NOT a bare
|
|
138
|
-
// scalar) → the native map/struct type (#108-map: a `{map:V}` write port lowers to map[string]V).
|
|
139
|
-
const composite = portCompositeRef(node, inputPorts, asBinding, plan);
|
|
140
|
-
if (composite !== null)
|
|
141
|
-
return renderTypeRef(composite);
|
|
142
|
-
const ft = inferPortType(node, inputPorts, asBinding);
|
|
143
|
-
if (ft === "Value") {
|
|
144
|
-
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.`);
|
|
145
|
-
}
|
|
146
|
-
return ft;
|
|
59
|
+
function portFieldGoType(node, comp, plan, typedNodes, where = "port", opts) {
|
|
60
|
+
const c = compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), where, opts);
|
|
61
|
+
return c.renderedType ?? renderTypeRef(c.ref);
|
|
147
62
|
}
|
|
148
|
-
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
ref = goTypedInternals.typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan).ref;
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
return null;
|
|
63
|
+
/**
|
|
64
|
+
* goNativeResolveHead — the ONE ref-head resolver the runtime-free native-expr compiler reads, for cond
|
|
65
|
+
* `if`/branches, a map/fanout `when` guard, AND a port (go twin — Go's value semantics need no ownership
|
|
66
|
+
* ceremony). A head is a `$as` element binding, a prior body node's typed local, or an input port.
|
|
67
|
+
*/
|
|
68
|
+
function goNativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
|
|
69
|
+
return (head) => {
|
|
70
|
+
if (opts?.asBinding !== undefined && opts.asBase !== undefined && head === opts.asBinding.name) {
|
|
71
|
+
return { expr: opts.asBase, ref: opts.asBinding.ref };
|
|
162
72
|
}
|
|
73
|
+
if (isPrior(head))
|
|
74
|
+
return { expr: typedLocal(head), ref: typedNodes.get(head) };
|
|
75
|
+
return goInputHeadRef(head, comp, plan);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* compilePortNode — lower a port expression to its native ports-struct field via the runtime-free
|
|
80
|
+
* native-expr compiler (native-expr.ts): the ONE port lowering. The port's field TYPE is the compiled
|
|
81
|
+
* `ref`, its initializer the compiled `expr` — so the two are derived from a SINGLE lowering and can
|
|
82
|
+
* never disagree. Any closed operator, a literal, a ref (input scalar/array/map, prior-node scalar/array,
|
|
83
|
+
* `$as` element field), a concat, a static array, an optional-input read — all resolve here. A port that
|
|
84
|
+
* cannot lower FAILS CLOSED loudly. A lowering that is FALLIBLE (hoists statements) is a covered SHAPE
|
|
85
|
+
* returned here as-is; only the value site (emitPortInit) rejects it, since a ports-struct literal cannot
|
|
86
|
+
* host statements — the coverage gate/type site must still see it as covered.
|
|
87
|
+
*/
|
|
88
|
+
function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
|
|
89
|
+
const resolveHead = goNativeResolveHead(comp, plan, typedNodes, isPrior, opts);
|
|
90
|
+
try {
|
|
91
|
+
// zeroOut is unreachable: a fallible port is rejected at emitPortInit (a ports-struct literal hosts no
|
|
92
|
+
// statements), so the backend never renders an early-return with it.
|
|
93
|
+
return new NativeExprCompiler(makeGoExprBackend("", resolveHead, plan)).compilePort(node);
|
|
163
94
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (et !== undefined)
|
|
169
|
-
return { kind: "map", value: deriveTypeRef(et, plan) };
|
|
170
|
-
}
|
|
171
|
-
return null;
|
|
95
|
+
catch (e) {
|
|
96
|
+
if (!(e instanceof GeneratorFailure))
|
|
97
|
+
throw e;
|
|
98
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} ${JSON.stringify(node)} does not lower to a native Go value: ${e.message}`);
|
|
172
99
|
}
|
|
173
|
-
if (ref === null)
|
|
174
|
-
return null;
|
|
175
|
-
if (ref.kind === "map" || ref.kind === "named")
|
|
176
|
-
return ref;
|
|
177
|
-
return null;
|
|
178
100
|
}
|
|
179
|
-
/**
|
|
180
|
-
*
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (asBinding !== undefined && elemBaseExpr !== undefined && e.path[0] === asBinding.name) {
|
|
187
|
-
let acc;
|
|
101
|
+
/** the prior-node typed-local TypeRef map for a component (control-gate nodes are typeless; a map node
|
|
102
|
+
* carries its produced-array ref). Shared by coverage, the struct surface, and the runner. */
|
|
103
|
+
function buildTypedNodes(comp, plan) {
|
|
104
|
+
const typedNodes = new Map();
|
|
105
|
+
for (const n of comp.body) {
|
|
106
|
+
if (isControlGate(n))
|
|
107
|
+
continue;
|
|
188
108
|
try {
|
|
189
|
-
|
|
109
|
+
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
|
|
190
110
|
}
|
|
191
111
|
catch {
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
if (acc.ref.kind !== "map" && acc.ref.kind !== "named")
|
|
195
|
-
return null;
|
|
196
|
-
return acc.expr;
|
|
197
|
-
}
|
|
198
|
-
// a WHOLE-port ref to an input MAP port (with elemType) — valid at top level OR inside a map node (a
|
|
199
|
-
// ref head that is NOT the `$as` element binding). Reads the native map[string]V off the input struct.
|
|
200
|
-
if ((asBinding === undefined || e.path[0] !== asBinding.name) && e.path.length === 1) {
|
|
201
|
-
const s = inputPorts[e.path[0]];
|
|
202
|
-
if (s !== undefined && s.type === "map" && s.elemType !== undefined) {
|
|
203
|
-
return `in.${goFieldName(e.path[0])}`;
|
|
112
|
+
/* untyped node — omit */
|
|
204
113
|
}
|
|
205
114
|
}
|
|
206
|
-
return
|
|
115
|
+
return typedNodes;
|
|
207
116
|
}
|
|
208
117
|
function staticArrElems(node) {
|
|
209
118
|
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
@@ -214,90 +123,16 @@ function staticArrElems(node) {
|
|
|
214
123
|
const arg = node.arr;
|
|
215
124
|
return Array.isArray(arg) ? arg : null;
|
|
216
125
|
}
|
|
217
|
-
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
* port's elemType), extended from the `map…over` element-source path to a plain componentRef port.
|
|
225
|
-
*
|
|
226
|
-
* A static string array is a DIFFERENT covered shape ([]string, staticStringArrElems) — handled ahead of
|
|
227
|
-
* this. An array port with NO resolvable elemType, an opt/multi-segment ref, or a non-array input schema
|
|
228
|
-
* resolves to null here → the caller keeps its existing fail-closed behaviour (never a silent box).
|
|
229
|
-
* Requires a TypePlan (an obj elemType maps to a named decl); without one it stays null (fail-closed).
|
|
230
|
-
*/
|
|
231
|
-
function portArrayElemRef(node, inputPorts, plan) {
|
|
232
|
-
if (plan === undefined)
|
|
233
|
-
return null;
|
|
234
|
-
if (staticStringArrElems(node) !== null)
|
|
235
|
-
return null; // the static-string-array port ([]string) — not this shape.
|
|
236
|
-
const e = classifyExpr(node);
|
|
237
|
-
if (e.kind !== "ref" || e.opt || e.path.length !== 1)
|
|
238
|
-
return null;
|
|
239
|
-
const schema = inputPorts?.[e.path[0]];
|
|
240
|
-
if (schema === undefined || (schema.type !== "array" && schema.type !== "arr"))
|
|
241
|
-
return null;
|
|
242
|
-
const et = schema.elemType;
|
|
243
|
-
if (et === undefined)
|
|
244
|
-
return null;
|
|
245
|
-
return deriveTypeRef(et, plan);
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* isNativeRawComponent — a component covered by the combined read emitter. Strictly sequential
|
|
249
|
-
* (no real concurrency), body is ALL componentRef (no map/cond), default 'fail'/'continue'
|
|
250
|
-
* policy, relationKind is single/connection or absent, and EVERY node carries outType + the
|
|
251
|
-
* component carries outputType. relationKind:single / bindField ARE the point — inline exec makes
|
|
252
|
-
* the child-present decision from the real parent result. Ports AND the output are evaluated via
|
|
253
|
-
* the primitive SSoT over a Value-space portScope (so a dynamic operator in a port or an
|
|
254
|
-
* obj-assembly output is fine — the SSoT handles fail-closed semantics; only the RESULT plane +
|
|
255
|
-
* PORT container go native). A covered SHAPE that lacks outType is a fail-closed error raised at
|
|
256
|
-
* emit (see assertTypedCoverage) — never a silent re-box.
|
|
257
|
-
*/
|
|
258
|
-
/**
|
|
259
|
-
* numLiteralGoExpr — a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
|
|
260
|
-
* `classifyExpr` deliberately leaves a bare number `dynamic` (its checked int/float range rules are
|
|
261
|
-
* the evaluate SSoT, expr.go evalNumberLiteral), so a plain number never reaches the str/bool/null
|
|
262
|
-
* static branch. But a number literal IS statically resolvable to a native Go Value: the SAME
|
|
263
|
-
* range check the runtime applies is performed HERE at generation time (integral literal in the
|
|
264
|
-
* safe 2^53-1 window → int64; a fractional/exponent literal → the checked float64). Returns the Go
|
|
265
|
-
* expression for the literal's Value, or null when the node is not a bare number OR the integral
|
|
266
|
-
* literal is out of the safe window (that out-of-range case keeps the runtime INVALID_LITERAL
|
|
267
|
-
* semantics — the port is NOT covered natively, so the whole component falls through, never a
|
|
268
|
-
* silently-wrong inline). Mirrors coderuntime.evalNumberLiteral exactly.
|
|
269
|
-
*/
|
|
270
|
-
const NUM_SAFE_INT = 9007199254740991; // 2^53 - 1 (expr.go safeInt)
|
|
271
|
-
function numLiteralGoExpr(node) {
|
|
272
|
-
if (typeof node !== "number" || !Number.isFinite(node))
|
|
273
|
-
return null;
|
|
274
|
-
if (Number.isInteger(node)) {
|
|
275
|
-
if (node < -NUM_SAFE_INT || node > NUM_SAFE_INT)
|
|
276
|
-
return null; // out of safe window → not covered (runtime INVALID_LITERAL)
|
|
277
|
-
return `${PKG}.Value(int64(${node}))`;
|
|
126
|
+
/** Does a port lower to a native value? The ELIGIBILITY gate ASKS the one port lowering (compilePortNode
|
|
127
|
+
* / native-expr) whether it can lower this port, rather than re-deriving the shape rules — so the gate
|
|
128
|
+
* and the emitter can never disagree. A port that does not lower (or lowers fallibly) fails closed. */
|
|
129
|
+
function portIsStatic(node, comp, plan, typedNodes, asBinding) {
|
|
130
|
+
try {
|
|
131
|
+
compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), "port", asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined);
|
|
132
|
+
return true;
|
|
278
133
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
/** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
|
|
283
|
-
* resolvable to a native Go Value with no NewObj / no evaluate interpreter. */
|
|
284
|
-
function portIsStatic(node) {
|
|
285
|
-
const arr = staticArrElems(node);
|
|
286
|
-
if (arr !== null)
|
|
287
|
-
return arr.every(portIsStatic);
|
|
288
|
-
if (numLiteralGoExpr(node) !== null)
|
|
289
|
-
return true; // bare number literal (e.g. Query `limit`)
|
|
290
|
-
const e = classifyExpr(node);
|
|
291
|
-
switch (e.kind) {
|
|
292
|
-
case "str":
|
|
293
|
-
case "bool":
|
|
294
|
-
case "null":
|
|
295
|
-
case "ref":
|
|
296
|
-
return true;
|
|
297
|
-
case "concat":
|
|
298
|
-
return e.parts.every((p) => portIsStatic(reconstructG(p)));
|
|
299
|
-
default:
|
|
300
|
-
return false;
|
|
134
|
+
catch {
|
|
135
|
+
return false; // an eligibility probe: any lowering failure ⇒ not native-covered here.
|
|
301
136
|
}
|
|
302
137
|
}
|
|
303
138
|
/** The output must lower to a typed struct/value with no Value-scope read: a ref whose head is a
|
|
@@ -345,8 +180,8 @@ function isFullyTyped(comp) {
|
|
|
345
180
|
}
|
|
346
181
|
return true;
|
|
347
182
|
}
|
|
348
|
-
function isNativeRaw(comp) {
|
|
349
|
-
return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
|
|
183
|
+
function isNativeRaw(comp, plan) {
|
|
184
|
+
return isSequentialComponentRefRead(comp, plan) && isFullyTyped(comp);
|
|
350
185
|
}
|
|
351
186
|
/**
|
|
352
187
|
* coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet). A BATCHED `map` node whose:
|
|
@@ -361,7 +196,7 @@ function isNativeRaw(comp) {
|
|
|
361
196
|
* element struct by copying the over element's typed fields + materializing the into field from the
|
|
362
197
|
* aligned batch raw — NO boxed Value on any plane.
|
|
363
198
|
*/
|
|
364
|
-
function coveredMapNode(n, bodyIds, comp) {
|
|
199
|
+
function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
|
|
365
200
|
if (n === null || typeof n !== "object" || !("map" in n))
|
|
366
201
|
return false;
|
|
367
202
|
const m = n.map;
|
|
@@ -398,11 +233,22 @@ function coveredMapNode(n, bodyIds, comp) {
|
|
|
398
233
|
return false;
|
|
399
234
|
}
|
|
400
235
|
const ports = m.ports;
|
|
236
|
+
const asBinding = mapAsBinding(comp, n, typedNodes, plan);
|
|
401
237
|
for (const p of Object.keys(ports))
|
|
402
|
-
if (!portIsStatic(ports[p]))
|
|
238
|
+
if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
|
|
403
239
|
return false;
|
|
404
240
|
return true;
|
|
405
241
|
}
|
|
242
|
+
/** the `$as` element binding for a covered map/fanout node's element ports (name + over-element TypeRef),
|
|
243
|
+
* or undefined when the over element type does not resolve (the node is then uncovered upstream). */
|
|
244
|
+
function mapAsBinding(comp, node, typedNodes, plan) {
|
|
245
|
+
try {
|
|
246
|
+
return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
406
252
|
/**
|
|
407
253
|
* coveredFanoutNode (v3) — a FANOUT node (first-class `@refs` id-list fan-out) is covered when:
|
|
408
254
|
* - over is a static ref to a PRIOR BODY NODE's typed arr field OR an INPUT array port with elemType,
|
|
@@ -411,7 +257,7 @@ function coveredMapNode(n, bodyIds, comp) {
|
|
|
411
257
|
* The fanout de-boxes to: one deduped batched dispatch → first-seen dedupe (by the elem's dedupeKey
|
|
412
258
|
* field) + dangling drop + implicitSource strip + connection wrap {items, cursor:nil} — NO boxed Value.
|
|
413
259
|
*/
|
|
414
|
-
function coveredFanoutNode(n, bodyIds, comp) {
|
|
260
|
+
function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
|
|
415
261
|
if (n === null || typeof n !== "object" || !("fanout" in n))
|
|
416
262
|
return false;
|
|
417
263
|
const f = n.fanout;
|
|
@@ -431,8 +277,15 @@ function coveredFanoutNode(n, bodyIds, comp) {
|
|
|
431
277
|
return false;
|
|
432
278
|
}
|
|
433
279
|
const ports = f.ports;
|
|
280
|
+
let asBinding;
|
|
281
|
+
try {
|
|
282
|
+
asBinding = { name: f.as, ref: fanoutOverElemInfo(comp, n, typedNodes, plan).elemRef, plan };
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
asBinding = undefined;
|
|
286
|
+
}
|
|
434
287
|
for (const p of Object.keys(ports))
|
|
435
|
-
if (!portIsStatic(ports[p]))
|
|
288
|
+
if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
|
|
436
289
|
return false;
|
|
437
290
|
return true;
|
|
438
291
|
}
|
|
@@ -441,8 +294,8 @@ function coveredFanoutNode(n, bodyIds, comp) {
|
|
|
441
294
|
* reads. A real-concurrency plan whose parallel members include a map / bindField-relation child is
|
|
442
295
|
* NOT covered here (falls through) — the static-parallel orchestration covers the componentRef fan-out
|
|
443
296
|
* (the GroupAccessRead shape); a parallel map/relation-child stage is a further shape (out of scope). */
|
|
444
|
-
function isSequentialComponentRefRead(comp) {
|
|
445
|
-
return coverageRejectReason(comp) === null;
|
|
297
|
+
function isSequentialComponentRefRead(comp, plan) {
|
|
298
|
+
return coverageRejectReason(comp, plan) === null;
|
|
446
299
|
}
|
|
447
300
|
/**
|
|
448
301
|
* coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89). Returns
|
|
@@ -452,10 +305,11 @@ function isSequentialComponentRefRead(comp) {
|
|
|
452
305
|
* never disagree. This is the message the fail-closed dispatch surfaces per non-native component — the
|
|
453
306
|
* user's signal to add native coverage, fix a genuinely-dynamic spec, or use the --ir dynamic surface.
|
|
454
307
|
*/
|
|
455
|
-
function coverageRejectReason(comp) {
|
|
308
|
+
function coverageRejectReason(comp, plan) {
|
|
456
309
|
const ep = execPlan(comp);
|
|
457
310
|
if (ep === null)
|
|
458
311
|
return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
|
|
312
|
+
const typedNodes = buildTypedNodes(comp, plan);
|
|
459
313
|
// A parallel-stage member must be a componentRef node (no map) so the static parallel orchestration
|
|
460
314
|
// is a clean fan-out — the bounded-concurrency error-precedence protocol is proven for that shape.
|
|
461
315
|
// A map child inside a parallel stage still falls through (its per-element loop is a further shape).
|
|
@@ -500,7 +354,7 @@ function coverageRejectReason(comp) {
|
|
|
500
354
|
if ("fanout" in n) {
|
|
501
355
|
// v3: a first-class fanout node (deduped connection list-in fan-out) IS covered when its over is a
|
|
502
356
|
// covered ref (prior-node arr / input array with elemType) and its element ports are static.
|
|
503
|
-
if (!coveredFanoutNode(n, bodyIds, comp))
|
|
357
|
+
if (!coveredFanoutNode(n, bodyIds, comp, plan, typedNodes))
|
|
504
358
|
return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
|
|
505
359
|
continue;
|
|
506
360
|
}
|
|
@@ -508,7 +362,7 @@ function coverageRejectReason(comp) {
|
|
|
508
362
|
// #86 pt2 batched map...into / #93 shape 2/3 / #108 guarded map (no into) / map-over-input (with
|
|
509
363
|
// elemType) ARE covered. A non-covered map shape (guard+into heterogeneous / over-input without
|
|
510
364
|
// elemType) falls through to the loud fail-closed reject.
|
|
511
|
-
if (!coveredMapNode(n, bodyIds, comp))
|
|
365
|
+
if (!coveredMapNode(n, bodyIds, comp, plan, typedNodes))
|
|
512
366
|
return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
|
|
513
367
|
continue;
|
|
514
368
|
}
|
|
@@ -539,7 +393,7 @@ function coverageRejectReason(comp) {
|
|
|
539
393
|
if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
540
394
|
return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
|
|
541
395
|
for (const p of Object.keys(cr.ports))
|
|
542
|
-
if (portIsStatic(cr.ports[p]) === false)
|
|
396
|
+
if (portIsStatic(cr.ports[p], comp, plan, typedNodes) === false)
|
|
543
397
|
return `node '${n.id}' port '${p}' is not statically resolvable`;
|
|
544
398
|
}
|
|
545
399
|
if (!outputIsNativeLowerable(comp))
|
|
@@ -547,21 +401,21 @@ function coverageRejectReason(comp) {
|
|
|
547
401
|
return null;
|
|
548
402
|
}
|
|
549
403
|
/** Fail closed if a covered read SHAPE lacks the typed annotations (no silent boxed re-box). */
|
|
550
|
-
function assertTypedCoverage(comp) {
|
|
551
|
-
if (!isSequentialComponentRefRead(comp))
|
|
404
|
+
function assertTypedCoverage(comp, plan) {
|
|
405
|
+
if (!isSequentialComponentRefRead(comp, plan))
|
|
552
406
|
return;
|
|
553
407
|
if (isFullyTyped(comp))
|
|
554
408
|
return;
|
|
555
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).`);
|
|
556
410
|
}
|
|
557
411
|
// ── native ports struct + PortReader ──────────────────────────────────────────────────
|
|
558
|
-
function emitPortsStruct(comp, node,
|
|
412
|
+
function emitPortsStruct(comp, node, plan, typedNodes, asBinding) {
|
|
559
413
|
const structName = portsStructName(comp.name, node.id);
|
|
560
414
|
const portNames = Object.keys(node.ports);
|
|
561
415
|
if (portNames.length === 0) {
|
|
562
416
|
return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
|
|
563
417
|
}
|
|
564
|
-
const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp
|
|
418
|
+
const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp, plan, typedNodes, `port '${p}' of node '${node.id}'`, asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined));
|
|
565
419
|
const fieldNames = portNames.map(goPortFieldName);
|
|
566
420
|
const fields = portNames
|
|
567
421
|
.map((p, i) => `\t${fieldNames[i]} ${fieldTypes[i]} // ${JSON.stringify(p)}`)
|
|
@@ -775,7 +629,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
|
|
|
775
629
|
// struct element). A pure-static/concat port ignores the binding.
|
|
776
630
|
const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
777
631
|
const asBinding = { name: m.as, ref: elemRef, plan };
|
|
778
|
-
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports },
|
|
632
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, plan, typedNodes, asBinding);
|
|
779
633
|
const batchStruct = `${structName}_batch`;
|
|
780
634
|
return `${elemStruct}
|
|
781
635
|
|
|
@@ -793,7 +647,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
|
|
|
793
647
|
const structName = portsStructName(comp.name, node.id);
|
|
794
648
|
const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
|
|
795
649
|
const asBinding = { name: f.as, ref: elemRef, plan };
|
|
796
|
-
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports },
|
|
650
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, plan, typedNodes, asBinding);
|
|
797
651
|
const batchStruct = `${structName}_batch`;
|
|
798
652
|
return `${elemStruct}
|
|
799
653
|
|
|
@@ -940,43 +794,6 @@ function mapOverElemInfo(comp, node, typedNodes, plan) {
|
|
|
940
794
|
function mapOverElemType(comp, node, typedNodes, plan) {
|
|
941
795
|
return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
|
|
942
796
|
}
|
|
943
|
-
/**
|
|
944
|
-
* priorNodeArrElemInfo (#129) — resolve a leaf port that is a `ref` into a PRIOR BODY NODE whose
|
|
945
|
-
* annotated `outType` is (or reaches, through a field path) a native array, to its element TypeRef +
|
|
946
|
-
* the native Go slice expression that yields it. This GENERALIZES the `map…over` prior-node-arr
|
|
947
|
-
* dataflow (mapOverElemInfo's first branch) to ANY leaf input port: a fold/dedup leaf whose `items`
|
|
948
|
-
* port is `{ref:["<priorNode>"]}` (or `.items` of a prior connection node) now lowers to `[]ElemT`
|
|
949
|
-
* fed straight from the prior node's typed local — ZERO boxed Value on the read hot path.
|
|
950
|
-
*
|
|
951
|
-
* The element type comes STRICTLY from the prior node's EXPLICIT `outType` annotation walked through
|
|
952
|
-
* typedFieldAccess (BC does NOT infer — consumer-interface C3 / [[graphddb-typed-native-static-link]]):
|
|
953
|
-
* if the head is not a prior body node, or its outType (through the field path) is not an `arr`, this
|
|
954
|
-
* returns null and the caller keeps its existing fail-closed behaviour (never a silent box). A
|
|
955
|
-
* single-segment INPUT-array port (elemType) is a DIFFERENT covered shape (portArrayElemRef) — handled
|
|
956
|
-
* ahead of this. Requires the typedNodes map (prior-node outType TypeRefs) and a TypePlan.
|
|
957
|
-
*/
|
|
958
|
-
function priorNodeArrElemInfo(node, typedNodes, plan) {
|
|
959
|
-
if (typedNodes === undefined || plan === undefined)
|
|
960
|
-
return null;
|
|
961
|
-
if (staticArrElems(node) !== null)
|
|
962
|
-
return null; // a static array literal — not a prior-node ref.
|
|
963
|
-
const e = classifyExpr(node);
|
|
964
|
-
if (e.kind !== "ref" || e.opt || e.path.length < 1)
|
|
965
|
-
return null;
|
|
966
|
-
if (!typedNodes.has(e.path[0]))
|
|
967
|
-
return null; // head is not a prior body node → not this shape.
|
|
968
|
-
const baseRef = typedNodes.get(e.path[0]);
|
|
969
|
-
let acc;
|
|
970
|
-
try {
|
|
971
|
-
acc = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
|
|
972
|
-
}
|
|
973
|
-
catch {
|
|
974
|
-
return null; // the field path does not resolve on the prior node's outType → fail-closed at caller.
|
|
975
|
-
}
|
|
976
|
-
if (acc.ref.kind !== "arr")
|
|
977
|
-
return null; // the prior-node result (through the path) is not an array.
|
|
978
|
-
return { elemRef: acc.ref.elem, overExpr: acc.expr };
|
|
979
|
-
}
|
|
980
797
|
function classifyPort(node) {
|
|
981
798
|
if (staticArrElems(node) !== null)
|
|
982
799
|
return "static";
|
|
@@ -1030,43 +847,31 @@ function reconstructG(e) {
|
|
|
1030
847
|
function inStructName(compName) {
|
|
1031
848
|
return `In_${sanitize(compName)}`;
|
|
1032
849
|
}
|
|
1033
|
-
/**
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
return `${PKG}.Value`;
|
|
1056
|
-
}
|
|
1057
|
-
case "map": {
|
|
1058
|
-
// an input MAP port (dynamic string keys, declared value elemType) lowers to a native map[string]ElemT.
|
|
1059
|
-
const et = schema.elemType;
|
|
1060
|
-
if (et !== undefined && plan !== undefined)
|
|
1061
|
-
return `map[string]${renderTypeRef(deriveTypeRef(et, plan))}`;
|
|
1062
|
-
return `${PKG}.Value`;
|
|
1063
|
-
}
|
|
1064
|
-
default:
|
|
1065
|
-
return `${PKG}.Value`;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
/** The scalar kind an input port declares, or undefined if it is not a native scalar. */
|
|
850
|
+
/**
|
|
851
|
+
* inputPortGoType — the native Go type for an input port's declared type. Scalars lower to the concrete
|
|
852
|
+
* scalar; `array`/`map` with a declared elemType to `[]ElemT` / `map[string]ElemT`; an OPTIONAL port
|
|
853
|
+
* (`{opt:T}` → `required:false`) to `*T` — the native representation of "the value is absent" (nil).
|
|
854
|
+
* A declared type with no native lowering stays the boxed Value.
|
|
855
|
+
*
|
|
856
|
+
* An OPTIONAL port whose inner type has no native lowering FAILS CLOSED: a boxed `Value` cannot carry
|
|
857
|
+
* the presence distinction on the covered plane, and emitting the bare inner type would silently turn
|
|
858
|
+
* "absent" into a zero value. A port whose optionality cannot be represented is never silently coerced.
|
|
859
|
+
*/
|
|
860
|
+
function inputPortGoType(schema, plan, where = "input port") {
|
|
861
|
+
const ref = inputPortTypeRef(schema, plan);
|
|
862
|
+
if (ref !== undefined)
|
|
863
|
+
return renderTypeRef(ref);
|
|
864
|
+
if (inputPortIsOptional(schema))
|
|
865
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Go lowering, so "absent" has no native representation (a boxed Value would be an escape, and the bare inner type would silently read absent as a zero value). Declare an elemType for an array/map, or use a natively-lowerable scalar.`);
|
|
866
|
+
return `${PKG}.Value`;
|
|
867
|
+
}
|
|
868
|
+
/** The scalar kind a REQUIRED input port declares, or undefined when it is not a required native scalar.
|
|
869
|
+
* An OPTIONAL port is deliberately NOT a scalar kind here: its native type is `*T`, and the callers of
|
|
870
|
+
* this function build REQUIRED scalar slots (`in.<Field>` read straight into a non-opt port field).
|
|
871
|
+
* Answering "int64" for an `{opt:"int"}` port is exactly how "absent" would silently become `0`. */
|
|
1069
872
|
function inputScalarKind(schema) {
|
|
873
|
+
if (inputPortIsOptional(schema))
|
|
874
|
+
return undefined;
|
|
1070
875
|
switch (schema?.type) {
|
|
1071
876
|
case "string":
|
|
1072
877
|
case "literal": // a `"literal"` union is a constrained string (see inputPortGoType).
|
|
@@ -1091,7 +896,7 @@ function emitInStruct(comp, plan) {
|
|
|
1091
896
|
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
|
|
1092
897
|
}
|
|
1093
898
|
const names = ports.map((p) => goFieldName(p));
|
|
1094
|
-
const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan));
|
|
899
|
+
const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`));
|
|
1095
900
|
const nameW = Math.max(0, ...names.map((n) => n.length));
|
|
1096
901
|
const typeW = Math.max(0, ...types.map((t) => t.length));
|
|
1097
902
|
const fields = ports
|
|
@@ -1132,6 +937,15 @@ function emitInDecoder(comp, plan) {
|
|
|
1132
937
|
const scalar = inputScalarKind(schema);
|
|
1133
938
|
const et = schema.elemType;
|
|
1134
939
|
lines.push(`\tif v, ok := input.Get(${JSON.stringify(p)}); ok {`);
|
|
940
|
+
if (inputPortIsOptional(schema)) {
|
|
941
|
+
// An OPTIONAL port decodes into its native *T. materializeExpr's opt de-box IS the wire rule:
|
|
942
|
+
// a nil Value (null) → nil (absent), any other value → a pointer to the materialized inner. A key
|
|
943
|
+
// that never appears leaves the struct's zero value (nil) — the same absent value. Checked FIRST:
|
|
944
|
+
// optionality wraps every inner type (an optional array is *[]T, not []T).
|
|
945
|
+
lines.push(`\t\tin.${field} = ${goTypedInternals.materializeExpr("v", inputPortTypeRef(schema, plan), plan)}`);
|
|
946
|
+
lines.push(`\t}`);
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
1135
949
|
if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
1136
950
|
// #108: decode the boxed Value array into the native []ElemT (test glue — materialize each element
|
|
1137
951
|
// via the typed materializer; a non-array or non-conforming element is a fail-closed decode error).
|
|
@@ -1175,115 +989,17 @@ function emitInDecoder(comp, plan) {
|
|
|
1175
989
|
return lines.join("\n");
|
|
1176
990
|
}
|
|
1177
991
|
/**
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1180
|
-
* `
|
|
1181
|
-
*
|
|
1182
|
-
*
|
|
1183
|
-
* are statically strings), emitting `p0 + p1 + …`. Returns null (fall back to the Value path) otherwise.
|
|
992
|
+
* emitPortInit — the port field initializer, read off the ONE port lowering (compilePortNode / native-
|
|
993
|
+
* expr). The field's declared TYPE (portFieldGoType) is the SAME lowering's TypeRef, so type and value
|
|
994
|
+
* can never disagree. `isPrior` tests whether a ref head names a settled prior node at this position;
|
|
995
|
+
* `opts.asBinding`/`opts.asBase` carry a map/fanout element binding. A FALLIBLE lowering (hoisted
|
|
996
|
+
* statements) is rejected HERE — a ports-struct literal cannot host statements.
|
|
1184
997
|
*/
|
|
1185
|
-
function
|
|
1186
|
-
|
|
1187
|
-
if (
|
|
1188
|
-
return
|
|
1189
|
-
|
|
1190
|
-
switch (e.kind) {
|
|
1191
|
-
case "str":
|
|
1192
|
-
return want === "string" ? JSON.stringify(e.value) : null;
|
|
1193
|
-
case "bool":
|
|
1194
|
-
return want === "bool" ? (e.value ? "true" : "false") : null;
|
|
1195
|
-
case "null":
|
|
1196
|
-
return null; // a null literal is a Value(nil), never a required native scalar
|
|
1197
|
-
case "ref": {
|
|
1198
|
-
const sc = resolveRef(e.path[0], e.path.slice(1), e.opt);
|
|
1199
|
-
if (sc === null || sc.scalar !== want)
|
|
1200
|
-
return null;
|
|
1201
|
-
return sc.expr;
|
|
1202
|
-
}
|
|
1203
|
-
case "concat": {
|
|
1204
|
-
if (want !== "string")
|
|
1205
|
-
return null;
|
|
1206
|
-
const parts = [];
|
|
1207
|
-
for (const p of e.parts) {
|
|
1208
|
-
const sub = emitNativeScalar(reconstructG(p), "string", resolveRef);
|
|
1209
|
-
if (sub === null)
|
|
1210
|
-
return null;
|
|
1211
|
-
// parenthesize a `+` sub-expression so string concatenation binds correctly (a ref/literal is
|
|
1212
|
-
// atomic; a nested concat is already a single `a + b` — wrap to keep it one operand).
|
|
1213
|
-
parts.push(sub.includes(" + ") ? `(${sub})` : sub);
|
|
1214
|
-
}
|
|
1215
|
-
return parts.join(" + ");
|
|
1216
|
-
}
|
|
1217
|
-
default:
|
|
1218
|
-
return null;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
/**
|
|
1222
|
-
* emitPortInit — emit the port field build for one port as a FULLY NATIVE Go expression (bc#90 /
|
|
1223
|
-
* runtime-free). Every covered port lowers to a concrete Go value assigned straight into the ports
|
|
1224
|
-
* struct field — NO `dslcontracts.Value`, NO nvVE helper, NO `.(T)` type-assert:
|
|
1225
|
-
* - a static string array (projection) → `[]string{"a", "b", ...}` (change #2);
|
|
1226
|
-
* - a bare int/float number literal (e.g. Query `limit: 20`) → `int64(20)` / `float64(...)`;
|
|
1227
|
-
* - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
|
|
1228
|
-
* A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered
|
|
1229
|
-
* plane — a boxed escape would re-introduce the bc-runtime coupling this module removes). Returns the
|
|
1230
|
-
* struct field initializer (`Field: <expr>`). `resolveRef` resolves a ref head for the scalar path.
|
|
1231
|
-
*/
|
|
1232
|
-
function emitPortInit(pn, expr, fieldGoType, resolveRef, inputPorts, plan, typedNodes, asBinding, elemBaseExpr) {
|
|
1233
|
-
const fieldName = goPortFieldName(pn);
|
|
1234
|
-
// #108-map: a port that is a ref to a `$as` element MAP/NAMED field (or an input map port) → the typed
|
|
1235
|
-
// composite value directly off the element/input (native map[string]V / struct — ZERO boxed Value).
|
|
1236
|
-
if (plan !== undefined) {
|
|
1237
|
-
const compExpr = emitCompositePortValue(expr, inputPorts ?? {}, asBinding, plan, elemBaseExpr);
|
|
1238
|
-
if (compExpr !== null)
|
|
1239
|
-
return `${fieldName}: ${compExpr}`;
|
|
1240
|
-
}
|
|
1241
|
-
// #110: a componentRef port bound to a runtime array input port (with elemType) → the native []ElemT,
|
|
1242
|
-
// read straight from the concrete input struct (`in.<Field>`) — ZERO boxed Value on the read hot path.
|
|
1243
|
-
// The over-elem/element-source case (map…over) already resolves via mapOverElemInfo; this is the plain
|
|
1244
|
-
// componentRef port (an IN-list / array-bound WHERE head).
|
|
1245
|
-
if (inputPorts !== undefined && portArrayElemRef(expr, inputPorts, plan) !== null) {
|
|
1246
|
-
const e = classifyExpr(expr);
|
|
1247
|
-
if (e.kind === "ref" && e.path.length === 1) {
|
|
1248
|
-
return `${fieldName}: in.${goFieldName(e.path[0])}`;
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
// #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) →
|
|
1252
|
-
// the native []ElemT read straight off the prior node's typed local (`t_<node>.Field…`) — ZERO boxed
|
|
1253
|
-
// Value on the read hot path. The over-source case (map…over) resolves via mapOverElemInfo; this is
|
|
1254
|
-
// the plain leaf array input port (a fold/dedup leaf whose `items` = a prior node's array result).
|
|
1255
|
-
const priorArr = priorNodeArrElemInfo(expr, typedNodes, plan);
|
|
1256
|
-
if (priorArr !== null)
|
|
1257
|
-
return `${fieldName}: ${priorArr.overExpr}`;
|
|
1258
|
-
// change #2: a static string array (projection) → a native `[]string{...}` literal.
|
|
1259
|
-
const strArr = staticStringArrElems(expr);
|
|
1260
|
-
if (strArr !== null) {
|
|
1261
|
-
if (fieldGoType !== "[]string") {
|
|
1262
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90): port '${pn}' is a static string array but its field type is '${fieldGoType}' (expected []string).`);
|
|
1263
|
-
}
|
|
1264
|
-
return `${fieldName}: []string{${strArr.map((s) => JSON.stringify(s)).join(", ")}}`;
|
|
1265
|
-
}
|
|
1266
|
-
// a bare number literal (e.g. Query `limit: 20`): the SSoT range check ran in numLiteralGoExpr —
|
|
1267
|
-
// emit the concrete native int64/float64 (unwrap the dslcontracts.Value(...) box: it was only there
|
|
1268
|
-
// for the old boxed path; the range check is what matters, and it already passed).
|
|
1269
|
-
const num = numLiteralGoExpr(expr);
|
|
1270
|
-
if (num !== null) {
|
|
1271
|
-
if (fieldGoType === "int64")
|
|
1272
|
-
return `${fieldName}: int64(${expr})`;
|
|
1273
|
-
if (fieldGoType === "float64")
|
|
1274
|
-
return `${fieldName}: float64(${expr})`;
|
|
1275
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90): port '${pn}' is a number literal but its field type is '${fieldGoType}'.`);
|
|
1276
|
-
}
|
|
1277
|
-
// string / bool / scalar-input / concat → native scalar expression (no Value, no `.(T)`).
|
|
1278
|
-
const scalarWant = fieldGoType;
|
|
1279
|
-
const native = emitNativeScalar(expr, scalarWant, resolveRef);
|
|
1280
|
-
if (native !== null) {
|
|
1281
|
-
return `${fieldName}: ${native}`;
|
|
1282
|
-
}
|
|
1283
|
-
// FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would
|
|
1284
|
-
// re-couple the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the
|
|
1285
|
-
// component is not native-eligible and must regenerate on the boxed straight-line path.
|
|
1286
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): port '${pn}' (${JSON.stringify(expr)}) does not lower to a native Go value of type '${fieldGoType}'. The covered read plane admits NO boxed dslcontracts.Value escape — regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
|
|
998
|
+
function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
|
|
999
|
+
const c = compilePortNode(expr, comp, plan, typedNodes, isPrior, `port '${pn}'`, opts);
|
|
1000
|
+
if (c.stmts.length > 0)
|
|
1001
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): port '${pn}' ${JSON.stringify(expr)} lowers to a FALLIBLE expression (one that can raise INT_OVERFLOW / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS). Such an expression lowers to STATEMENTS carrying an early error-return, and a port is built inline in the ports-struct literal — a position that cannot host them. (A cond/guard position CAN host them, so the same expression is covered there — the boundary is the POSITION, not the operator.) Use a non-fallible port expression.`);
|
|
1002
|
+
return `${goPortFieldName(pn)}: ${c.expr}`;
|
|
1287
1003
|
}
|
|
1288
1004
|
/**
|
|
1289
1005
|
* emitParallelStageArm (bc#87) — EXPLICIT static parallel orchestration for a real-concurrency stage.
|
|
@@ -1306,7 +1022,7 @@ function emitPortInit(pn, expr, fieldGoType, resolveRef, inputPorts, plan, typed
|
|
|
1306
1022
|
*
|
|
1307
1023
|
* NO RunPlan / []OpSpec / planSpec walk: the staging, the bound, and which ops are parallel are baked.
|
|
1308
1024
|
*/
|
|
1309
|
-
function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags
|
|
1025
|
+
function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags) {
|
|
1310
1026
|
const lines = [];
|
|
1311
1027
|
const ids = stage.map((i) => comp.body[i].id);
|
|
1312
1028
|
lines.push(`\t// ── bc#87 real-concurrency stage {${ids.map((x) => JSON.stringify(x)).join(", ")}} — static parallel`);
|
|
@@ -1395,11 +1111,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1395
1111
|
}
|
|
1396
1112
|
}
|
|
1397
1113
|
const inits = [];
|
|
1398
|
-
const resolveRef = resolveRefAt(atPos);
|
|
1399
1114
|
for (const pn of Object.keys(node.ports)) {
|
|
1400
|
-
|
|
1401
|
-
const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
|
|
1402
|
-
inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef, comp.inputPorts, plan, typedNodes));
|
|
1115
|
+
inits.push(emitPortInit(pn, node.ports[pn], comp, plan, typedNodes, (h) => priorNodeAt(h, atPos)));
|
|
1403
1116
|
}
|
|
1404
1117
|
lines.push(`${ind}ps_${s} = ${structName}{${inits.join(", ")}}`);
|
|
1405
1118
|
lines.push(`${ind}run_${s} = true`);
|
|
@@ -1506,35 +1219,6 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1506
1219
|
// assert on a row value — the runner reads row.<Field> directly and copies each into the outType local.
|
|
1507
1220
|
lines.push(`func ${runnerName(comp.name)}[H ${handlerIfaceName(comp.name)}](handlers H, in ${inStructName(comp.name)}) (${outStructType}, error) {`);
|
|
1508
1221
|
lines.push(`\t_ = in`);
|
|
1509
|
-
// resolveNativeRef for this runner: a ref head is either a PRIOR NODE (read its typed struct field
|
|
1510
|
-
// directly) or an INPUT PORT (read the typed `in.<Field>`). A REQUIRED scalar resolves to a native
|
|
1511
|
-
// Go expression; an opt / non-scalar / refOpt resolves to null (falls back to the nvVE Value path).
|
|
1512
|
-
const resolveRefAt = (atPos) => (head, restPath, opt) => {
|
|
1513
|
-
if (opt)
|
|
1514
|
-
return null; // a refOpt may be nil at runtime — keep the Value path (opt-nil semantics)
|
|
1515
|
-
if (priorNodeAt(head, atPos)) {
|
|
1516
|
-
const baseRef = typedNodes.get(head);
|
|
1517
|
-
let acc;
|
|
1518
|
-
let expr;
|
|
1519
|
-
try {
|
|
1520
|
-
const r = goTypedInternals.typedFieldAccess(typedLocal(head), baseRef, restPath, plan);
|
|
1521
|
-
expr = r.expr;
|
|
1522
|
-
acc = r.ref;
|
|
1523
|
-
}
|
|
1524
|
-
catch {
|
|
1525
|
-
return null;
|
|
1526
|
-
}
|
|
1527
|
-
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1528
|
-
return null;
|
|
1529
|
-
return { expr, scalar: goScalar(acc.scalar) };
|
|
1530
|
-
}
|
|
1531
|
-
if (restPath.length !== 0)
|
|
1532
|
-
return null; // input.<field> path is a Value walk — not a native scalar
|
|
1533
|
-
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1534
|
-
if (scalar === undefined)
|
|
1535
|
-
return null;
|
|
1536
|
-
return { expr: `in.${goFieldName(head)}`, scalar };
|
|
1537
|
-
};
|
|
1538
1222
|
for (const k of order.keys()) {
|
|
1539
1223
|
const node = comp.body[order[k]];
|
|
1540
1224
|
const id = node.id;
|
|
@@ -1557,7 +1241,7 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1557
1241
|
// ascending-sorted stages), so we emit the block at the first member and skip the rest.
|
|
1558
1242
|
const stage = ep.parallelStageOf.get(i);
|
|
1559
1243
|
if (stage !== undefined && stage[0] === i) {
|
|
1560
|
-
lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags
|
|
1244
|
+
lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags));
|
|
1561
1245
|
k += stage.length - 1;
|
|
1562
1246
|
continue;
|
|
1563
1247
|
}
|
|
@@ -1629,11 +1313,8 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1629
1313
|
// dslcontracts.Value, ZERO nvVE helper, ZERO `.(T)`. A genuinely-dynamic port FAILS CLOSED.
|
|
1630
1314
|
const inits = [];
|
|
1631
1315
|
const boundArg = node.bindField !== undefined ? `bound_${s}` : "nil";
|
|
1632
|
-
const resolveRef = resolveRefAt(k);
|
|
1633
1316
|
for (const pn of portNames) {
|
|
1634
|
-
|
|
1635
|
-
const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
|
|
1636
|
-
inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef, comp.inputPorts, plan, typedNodes));
|
|
1317
|
+
inits.push(emitPortInit(pn, node.ports[pn], comp, plan, typedNodes, (h) => priorNodeAt(h, k)));
|
|
1637
1318
|
}
|
|
1638
1319
|
lines.push(`${indent}ports_${s} := ${structName}{${inits.join(", ")}}`);
|
|
1639
1320
|
const oc = `row_${s}`;
|
|
@@ -1726,67 +1407,15 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1726
1407
|
const mkGuard = (elemVar) => {
|
|
1727
1408
|
if (!hasGuard)
|
|
1728
1409
|
return null;
|
|
1729
|
-
const resolveHead = (
|
|
1730
|
-
|
|
1731
|
-
return { expr: elemVar, ref: overElemRef };
|
|
1732
|
-
if (priorHere(head))
|
|
1733
|
-
return { expr: typedLocal(head), ref: typedNodes.get(head) };
|
|
1734
|
-
return goInputHeadRef(head, comp, plan);
|
|
1735
|
-
};
|
|
1736
|
-
const be = makeGoExprBackend(zeroOut, resolveHead, plan);
|
|
1737
|
-
return new NativeExprCompiler(be).compileBool(m.when);
|
|
1410
|
+
const resolveHead = goNativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding: { name: asName, ref: overElemRef, plan }, asBase: elemVar });
|
|
1411
|
+
return new NativeExprCompiler(makeGoExprBackend(zeroOut, resolveHead, plan)).compileBool(m.when);
|
|
1738
1412
|
};
|
|
1739
1413
|
// emit the per-element native ports struct build (shared; `il` is the indent inside the element
|
|
1740
1414
|
// loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
|
|
1741
1415
|
const buildPorts = (il, elemVar) => {
|
|
1742
|
-
const out = [];
|
|
1743
|
-
const inits = [];
|
|
1744
|
-
// resolveNativeRef for a map element port: a ref head is the element binding ($as → the over element
|
|
1745
|
-
// typed struct field `elemVar.<Field>`), a PRIOR NODE (its typed struct field), or an INPUT PORT
|
|
1746
|
-
// (`in.<Field>`). A REQUIRED scalar lowers to a native Go expr; opt / non-scalar → null (Value path).
|
|
1747
|
-
const resolveRef = (head, restPath, opt) => {
|
|
1748
|
-
if (opt)
|
|
1749
|
-
return null;
|
|
1750
|
-
let baseExpr;
|
|
1751
|
-
let baseRef;
|
|
1752
|
-
if (head === asName) {
|
|
1753
|
-
baseExpr = elemVar;
|
|
1754
|
-
baseRef = overElemRef;
|
|
1755
|
-
}
|
|
1756
|
-
else if (priorHere(head)) {
|
|
1757
|
-
baseExpr = typedLocal(head);
|
|
1758
|
-
baseRef = typedNodes.get(head);
|
|
1759
|
-
}
|
|
1760
|
-
else {
|
|
1761
|
-
if (restPath.length !== 0)
|
|
1762
|
-
return null;
|
|
1763
|
-
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1764
|
-
if (scalar === undefined)
|
|
1765
|
-
return null;
|
|
1766
|
-
return { expr: `in.${goFieldName(head)}`, scalar };
|
|
1767
|
-
}
|
|
1768
|
-
let acc;
|
|
1769
|
-
let expr;
|
|
1770
|
-
try {
|
|
1771
|
-
const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
|
|
1772
|
-
expr = r.expr;
|
|
1773
|
-
acc = r.ref;
|
|
1774
|
-
}
|
|
1775
|
-
catch {
|
|
1776
|
-
return null;
|
|
1777
|
-
}
|
|
1778
|
-
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1779
|
-
return null;
|
|
1780
|
-
return { expr, scalar: goScalar(acc.scalar) };
|
|
1781
|
-
};
|
|
1782
1416
|
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
// #108-map: pass the element binding + base expr so a `$as` MAP/NAMED field port reads the typed composite.
|
|
1786
|
-
inits.push(emitPortInit(pn, m.ports[pn], fieldGoType, resolveRef, comp.inputPorts, plan, undefined, asBinding, elemLocal(s)));
|
|
1787
|
-
}
|
|
1788
|
-
out.push(`${il}ep_${s} := ${structName}{${inits.join(", ")}}`);
|
|
1789
|
-
return out;
|
|
1417
|
+
const inits = portNames.map((pn) => emitPortInit(pn, m.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: elemVar }));
|
|
1418
|
+
return [`${il}ep_${s} := ${structName}{${inits.join(", ")}}`];
|
|
1790
1419
|
};
|
|
1791
1420
|
const lines = [];
|
|
1792
1421
|
const closers = [];
|
|
@@ -1924,47 +1553,8 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
|
|
|
1924
1553
|
const dkGo = goFieldName(f.dedupeKey);
|
|
1925
1554
|
// per-id native ports build (same as a batched map's per-element ports). $as → the over id value.
|
|
1926
1555
|
const buildPorts = (il, elemVar) => {
|
|
1927
|
-
const inits = [];
|
|
1928
|
-
const resolveRef = (head, restPath, opt) => {
|
|
1929
|
-
if (opt)
|
|
1930
|
-
return null;
|
|
1931
|
-
let baseExpr;
|
|
1932
|
-
let baseRef;
|
|
1933
|
-
if (head === asName) {
|
|
1934
|
-
baseExpr = elemVar;
|
|
1935
|
-
baseRef = overElemRef;
|
|
1936
|
-
}
|
|
1937
|
-
else if (priorHere(head)) {
|
|
1938
|
-
baseExpr = typedLocal(head);
|
|
1939
|
-
baseRef = typedNodes.get(head);
|
|
1940
|
-
}
|
|
1941
|
-
else {
|
|
1942
|
-
if (restPath.length !== 0)
|
|
1943
|
-
return null;
|
|
1944
|
-
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1945
|
-
if (scalar === undefined)
|
|
1946
|
-
return null;
|
|
1947
|
-
return { expr: `in.${goFieldName(head)}`, scalar };
|
|
1948
|
-
}
|
|
1949
|
-
let acc;
|
|
1950
|
-
let expr;
|
|
1951
|
-
try {
|
|
1952
|
-
const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
|
|
1953
|
-
expr = r.expr;
|
|
1954
|
-
acc = r.ref;
|
|
1955
|
-
}
|
|
1956
|
-
catch {
|
|
1957
|
-
return null;
|
|
1958
|
-
}
|
|
1959
|
-
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1960
|
-
return null;
|
|
1961
|
-
return { expr, scalar: goScalar(acc.scalar) };
|
|
1962
|
-
};
|
|
1963
1556
|
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1964
|
-
|
|
1965
|
-
const fieldGoType = portFieldGoType(f.ports[pn], comp.inputPorts, `fanout port '${pn}' of node '${node.id}'`, asBinding, plan);
|
|
1966
|
-
inits.push(emitPortInit(pn, f.ports[pn], fieldGoType, resolveRef, comp.inputPorts, plan));
|
|
1967
|
-
}
|
|
1557
|
+
const inits = portNames.map((pn) => emitPortInit(pn, f.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: elemVar }));
|
|
1968
1558
|
return [`${il}ep_${s} := ${structName}{${inits.join(", ")}}`];
|
|
1969
1559
|
};
|
|
1970
1560
|
const lines = [];
|
|
@@ -2019,23 +1609,19 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
|
|
|
2019
1609
|
lines.push(c);
|
|
2020
1610
|
return lines.join("\n");
|
|
2021
1611
|
}
|
|
2022
|
-
/**
|
|
2023
|
-
*
|
|
2024
|
-
*
|
|
1612
|
+
/**
|
|
1613
|
+
* goInputHeadRef (#108) — resolve an INPUT port head for a native-expr ref (cond `if` / map `when` /
|
|
1614
|
+
* branches / an opt-lane port). The port's declared type comes from the shared `inputPortTypeRef` SSoT,
|
|
1615
|
+
* so an OPTIONAL port resolves as `{opt:…}` (native `*T`) and the compiler can see its optionality: a
|
|
1616
|
+
* null-compare becomes a real presence test and a coalesce a real deref-or-default, while a
|
|
1617
|
+
* REQUIRED-scalar slot fed by an opt fails closed. Go values are read directly off the input struct (no
|
|
1618
|
+
* ownership ceremony); a type with no native lowering → null (fail-closed).
|
|
1619
|
+
*/
|
|
2025
1620
|
function goInputHeadRef(head, comp, plan) {
|
|
2026
|
-
const
|
|
2027
|
-
if (
|
|
1621
|
+
const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
|
|
1622
|
+
if (ref === undefined)
|
|
2028
1623
|
return null;
|
|
2029
|
-
|
|
2030
|
-
if (sc !== undefined) {
|
|
2031
|
-
const scalarRef = { kind: "scalar", scalar: sc === "int64" ? "int" : sc === "float64" ? "float" : sc };
|
|
2032
|
-
return { expr: `in.${goFieldName(head)}`, ref: scalarRef };
|
|
2033
|
-
}
|
|
2034
|
-
const et = schema.elemType;
|
|
2035
|
-
if ((schema.type === "array" || schema.type === "arr") && et !== undefined) {
|
|
2036
|
-
return { expr: `in.${goFieldName(head)}`, ref: { kind: "arr", elem: deriveTypeRef(et, plan) } };
|
|
2037
|
-
}
|
|
2038
|
-
return null;
|
|
1624
|
+
return { expr: `in.${goFieldName(head)}`, ref };
|
|
2039
1625
|
}
|
|
2040
1626
|
/**
|
|
2041
1627
|
* emitCondArm — the struct-native exec of a covered cond node (#108). Mirrors run_behavior's cond
|
|
@@ -2049,11 +1635,7 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut)
|
|
|
2049
1635
|
const c = node.cond;
|
|
2050
1636
|
const s = sanitize(node.id);
|
|
2051
1637
|
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
2052
|
-
const resolveHead = (
|
|
2053
|
-
if (priorHere(head))
|
|
2054
|
-
return { expr: typedLocal(head), ref: typedNodes.get(head) };
|
|
2055
|
-
return goInputHeadRef(head, comp, plan);
|
|
2056
|
-
};
|
|
1638
|
+
const resolveHead = goNativeResolveHead(comp, plan, typedNodes, priorHere);
|
|
2057
1639
|
const be = makeGoExprBackend(zeroOut, resolveHead, plan);
|
|
2058
1640
|
const compiler = new NativeExprCompiler(be);
|
|
2059
1641
|
const cond = compiler.compileBool(c.if);
|
|
@@ -2276,11 +1858,31 @@ function makeGoExprBackend(zeroOut, resolveHead, plan) {
|
|
|
2276
1858
|
notOp: (a) => `(!${a})`,
|
|
2277
1859
|
structLit: (name, inits) => `${name}{${inits.map((i) => `${goFieldName(i.field)}: ${i.expr}`).join(", ")}}`,
|
|
2278
1860
|
arrLit: (elemTy, items) => `[]${elemTy}{${items.join(", ")}}`,
|
|
1861
|
+
// a projection port of static string literals — bare `[]string{"a", "b"}` (Go string literals are
|
|
1862
|
+
// already allocation-free; no divergence from the general arr path, so no golden churn).
|
|
1863
|
+
staticStrArr: (literals) => ({ expr: `[]string{${literals.map((s) => JSON.stringify(s)).join(", ")}}`, type: "[]string" }),
|
|
2279
1864
|
// an opt (`*T`) NONE literal is `nil` (the inner type is not needed for the untyped nil in Go).
|
|
2280
1865
|
optNone: (_innerTy) => "nil",
|
|
2281
1866
|
// opt (`*T`) null-presence test: present = `(expr != nil)`, absent = `(expr == nil)`.
|
|
2282
1867
|
optIsSome: (expr) => `(${expr} != nil)`,
|
|
2283
1868
|
optIsNone: (expr) => `(${expr} == nil)`,
|
|
1869
|
+
// opt (`*T`) defaulted to a PURE default. Go has no `unwrap_or`, so this is the same immediately-
|
|
1870
|
+
// invoked func literal the `ternary` rendering uses (the established shape in this emitter): bind the
|
|
1871
|
+
// pointer ONCE, deref it when present, else yield the default. The default sits in the else branch,
|
|
1872
|
+
// so it is evaluated only when absent — run_behavior's lazy right side exactly.
|
|
1873
|
+
optUnwrapOr: (optExpr, defaultExpr, innerTy) => `func() ${innerTy} { if v := ${optExpr}; v != nil { return *v }; return ${defaultExpr} }()`,
|
|
1874
|
+
// opt defaulted to a FALLIBLE default — a statement if/else, NOT a func literal: the default's
|
|
1875
|
+
// hoisted `return zeroOut, err` early returns must leave the ENCLOSING fn, which a func literal
|
|
1876
|
+
// would swallow (it would return from the literal). Only the nil branch evaluates the default.
|
|
1877
|
+
optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
|
|
1878
|
+
`var ${temp} ${ty}`,
|
|
1879
|
+
`if ${bindTemp} := ${optExpr}; ${bindTemp} != nil {`,
|
|
1880
|
+
` ${temp} = *${bindTemp}`,
|
|
1881
|
+
`} else {`,
|
|
1882
|
+
...dStmts.map((x) => ` ${x}`),
|
|
1883
|
+
` ${temp} = ${dExpr}`,
|
|
1884
|
+
`}`,
|
|
1885
|
+
],
|
|
2284
1886
|
ternary: (cond, t, e, ty) => `func() ${ty} { if ${cond} { return ${t} }; return ${e} }()`,
|
|
2285
1887
|
shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
|
|
2286
1888
|
// and: temp=false; if left { <rStmts>; temp=rExpr }. or: temp=true; if !left { <rStmts>; temp=rExpr }.
|
|
@@ -2461,12 +2063,14 @@ var ExpectedSpecVersions = map[string]int64{"behavior": ${sv.behavior}, "express
|
|
|
2461
2063
|
}
|
|
2462
2064
|
// ── module assembly ──────────────────────────────────────────────────────────────────
|
|
2463
2065
|
function emit(ctx) {
|
|
2066
|
+
// The type plan is the SSoT the coverage gate + the port lowering both read (the gate ASKS the port
|
|
2067
|
+
// lowering whether a port lowers), so it is built up-front — before the coverage checks.
|
|
2068
|
+
const plan = buildTypePlan(ctx.ir);
|
|
2464
2069
|
// Fail closed if a covered read SHAPE lacks the typed annotations (no silent re-box).
|
|
2465
2070
|
for (const c of ctx.ir.components)
|
|
2466
|
-
assertTypedCoverage(c);
|
|
2467
|
-
const native = ctx.ir.components.filter(isNativeRaw);
|
|
2468
|
-
const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
|
|
2469
|
-
const plan = buildTypePlan(ctx.ir);
|
|
2071
|
+
assertTypedCoverage(c, plan);
|
|
2072
|
+
const native = ctx.ir.components.filter((c) => isNativeRaw(c, plan));
|
|
2073
|
+
const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c, plan));
|
|
2470
2074
|
const decls = emitTypeDecls(plan);
|
|
2471
2075
|
// #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler when it emits
|
|
2472
2076
|
// a fallible checked-arith helper call — determines whether the helper library + `math` import ship).
|
|
@@ -2481,11 +2085,7 @@ function emit(ctx) {
|
|
|
2481
2085
|
const runnerCodes = [];
|
|
2482
2086
|
for (const c of native) {
|
|
2483
2087
|
inStructs.push(emitInStruct(c, plan));
|
|
2484
|
-
const typedNodes =
|
|
2485
|
-
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
2486
|
-
for (const n of c.body)
|
|
2487
|
-
if (!isControlGate(n))
|
|
2488
|
-
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan)); // #125: control-gate typeless; map は produced-array ref に正規化
|
|
2088
|
+
const typedNodes = buildTypedNodes(c, plan);
|
|
2489
2089
|
for (const n of c.body) {
|
|
2490
2090
|
if ("fanout" in n) {
|
|
2491
2091
|
// v3: a fanout node emits a per-id element ports struct + a batch struct (like a batched map).
|
|
@@ -2501,7 +2101,7 @@ function emit(ctx) {
|
|
|
2501
2101
|
}
|
|
2502
2102
|
else {
|
|
2503
2103
|
// #129: pass typedNodes so a leaf array port fed by a prior node's arr outType types as []ElemT.
|
|
2504
|
-
structs.push(emitPortsStruct(c, n,
|
|
2104
|
+
structs.push(emitPortsStruct(c, n, plan, typedNodes));
|
|
2505
2105
|
}
|
|
2506
2106
|
}
|
|
2507
2107
|
// CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
|
|
@@ -2530,7 +2130,7 @@ function emit(ctx) {
|
|
|
2530
2130
|
// minimal, runtime-free header.
|
|
2531
2131
|
if (nonNative.length > 0) {
|
|
2532
2132
|
const rejects = nonNative
|
|
2533
|
-
.map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
|
|
2133
|
+
.map((c) => ` - component '${c.name}': ${coverageRejectReason(c, plan) ?? "not a covered native read"}`)
|
|
2534
2134
|
.join("\n");
|
|
2535
2135
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (native codegen) surface: ${nonNative.length} component(s) are NOT a covered native read:\n${rejects}\n\n` +
|
|
2536
2136
|
`The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
|
|
@@ -2588,8 +2188,8 @@ function emit(ctx) {
|
|
|
2588
2188
|
*/
|
|
2589
2189
|
export function goTypedNativeObserve(ir, runtimeImport) {
|
|
2590
2190
|
const components = ir.components;
|
|
2591
|
-
const native = components.filter(isNativeRaw);
|
|
2592
2191
|
const plan = buildTypePlan(ir);
|
|
2192
|
+
const native = components.filter((c) => isNativeRaw(c, plan));
|
|
2593
2193
|
const serializers = emitSerializers(plan);
|
|
2594
2194
|
const marshallers = goTypedInternals.emitMarshallers(plan);
|
|
2595
2195
|
const mustHelpers = emitMustHelpers(plan);
|
|
@@ -2713,17 +2313,25 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
|
2713
2313
|
const rowT = rawRowStructName(c.name, node.id);
|
|
2714
2314
|
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
2715
2315
|
const nodeBoundT = boundGoType(node, c, typedNodes, plan);
|
|
2716
|
-
// #129 (TEST glue):
|
|
2717
|
-
//
|
|
2718
|
-
//
|
|
2719
|
-
//
|
|
2720
|
-
//
|
|
2721
|
-
//
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2316
|
+
// #129 (TEST glue): support the reference scriptedHandlers' echo forms (PROTOCOL.md §3.5) — the
|
|
2317
|
+
// adapter normalizes `{"echo":"port","port":"X"}` / `{"echo":"items"}` to the port NAME, and the
|
|
2318
|
+
// scripted handler
|
|
2319
|
+
// ECHOES a NATIVE port back as its result. This makes a port-plane pin non-vacuous: the node's
|
|
2320
|
+
// observed result IS the native value that flowed into its ports struct, so a dropped / re-boxed /
|
|
2321
|
+
// zero-defaulted port diverges from run_behavior instead of passing. A port is echoable when its
|
|
2322
|
+
// native type IS the node's outType (that is exactly when the row's Val can hold it); `ports` is
|
|
2323
|
+
// named only when at least one port qualifies (else the param stays `_` so a non-echo node
|
|
2324
|
+
// compiles clean / unused-param-free).
|
|
2325
|
+
const echoable = echoablePorts(c, node, ref, plan, typedNodes);
|
|
2326
|
+
const portsParam = echoable.length > 0 ? "ports" : "_";
|
|
2327
|
+
// a NAMED outType row is FLATTENED (no Val) — echo a named port by copying the struct's fields;
|
|
2328
|
+
// a scalar/arr/opt outType row holds the port in its single Val field.
|
|
2329
|
+
const echoRow = (ep) => ref.kind === "named"
|
|
2330
|
+
? `${rowT}{${findDecl(plan, ref.name).fields.map((f) => `${goFieldName(f.name)}: ports.${goPortFieldName(ep)}.${goFieldName(f.name)}`).join(", ")}}`
|
|
2331
|
+
: `${rowT}{Val: ports.${goPortFieldName(ep)}}`;
|
|
2332
|
+
const echoBranch = echoable
|
|
2333
|
+
.map((ep) => `\tif src.echo == ${JSON.stringify(ep)} {\n\t\treturn ${echoRow(ep)}, true\n\t}\n`)
|
|
2334
|
+
.join("");
|
|
2727
2335
|
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, node.id)}(${portsParam} ${portsStructName(c.name, node.id)}, _ ${nodeBoundT}) (${rowT}, bool) {
|
|
2728
2336
|
src, ok := s.next(${JSON.stringify(node.component)})
|
|
2729
2337
|
if !ok {
|
|
@@ -2775,10 +2383,19 @@ func NewScriptedNativeRaw(spec *${PKG}.JObj) (*ScriptedNativeRaw, error) {
|
|
|
2775
2383
|
for _, e := range entries {
|
|
2776
2384
|
eo, _ := e.(*${PKG}.JObj)
|
|
2777
2385
|
// #129: an \`echo\` directive names an input port to echo back (mirrors the reference
|
|
2778
|
-
// scriptedHandlers'
|
|
2386
|
+
// scriptedHandlers' PROTOCOL.md §3.5 echo forms). It is drained together with the ok
|
|
2387
|
+
// payload. \`{"echo":"port","port":"X"}\` is normalized to the port NAME here, so the
|
|
2388
|
+
// generated node method just compares \`src.echo\` against its echoable port names —
|
|
2389
|
+
// \`{"echo":"items"}\` is already that shape.
|
|
2779
2390
|
echo := ""
|
|
2780
2391
|
if echoNode, has := eo.Get("echo"); has {
|
|
2781
2392
|
echo, _ = echoNode.(string)
|
|
2393
|
+
if echo == "port" {
|
|
2394
|
+
echo = ""
|
|
2395
|
+
if portNode, hasPort := eo.Get("port"); hasPort {
|
|
2396
|
+
echo, _ = portNode.(string)
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2782
2399
|
}
|
|
2783
2400
|
if okNode, has := eo.Get("ok"); has {
|
|
2784
2401
|
val, derr := ${PKG}.DecodeValue(okNode)
|
|
@@ -2862,6 +2479,14 @@ ${arms}
|
|
|
2862
2479
|
}
|
|
2863
2480
|
`;
|
|
2864
2481
|
}
|
|
2482
|
+
/** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
|
|
2483
|
+
* declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row (a scalar/arr/
|
|
2484
|
+
* opt row via its Val; a NAMED row via its flattened fields copied from the struct port). A port with a
|
|
2485
|
+
* different type is not echoable (the row could not hold it). */
|
|
2486
|
+
function echoablePorts(comp, node, ref, plan, typedNodes) {
|
|
2487
|
+
const want = renderTypeRef(ref);
|
|
2488
|
+
return Object.keys(node.ports).filter((p) => portFieldGoType(node.ports[p], comp, plan, typedNodes, `port '${p}'`) === want);
|
|
2489
|
+
}
|
|
2865
2490
|
/** emitDecodeRow — emit (once, deduped) a decoder `decodeRow_<comp>_<node><suffix>(v Value) <rowT>`
|
|
2866
2491
|
* that materializes the scripted ok Value into the concrete row struct (typed fields = the outType /
|
|
2867
2492
|
* elem-row type). Named: reuse the Value->struct marshaller + copy fields; scalar/arr/opt: materialize
|