behavior-contracts 0.8.4 → 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/generator/emit-typed-native-go.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-go.js +141 -616
- 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 +177 -664
- package/dist/generator/emit-typed-native-rust.js.map +1 -1
- package/dist/generator/native-expr.d.ts +23 -2
- package/dist/generator/native-expr.d.ts.map +1 -1
- package/dist/generator/native-expr.js +31 -2
- package/dist/generator/native-expr.js.map +1 -1
- package/package.json +1 -1
|
@@ -49,162 +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
|
-
// a ref to a REQUIRED scalar input port → that scalar. An OPTIONAL port has no required-scalar
|
|
76
|
-
// kind (its native type is *T) and is lowered by the opt lane ahead of this — reaching here with
|
|
77
|
-
// one means the shape is uncovered, and "Value" routes it to the caller's loud fail-closed.
|
|
78
|
-
if (e.path.length === 1) {
|
|
79
|
-
const sc = inputScalarKind(inputPorts[e.path[0]]);
|
|
80
|
-
if (sc !== undefined)
|
|
81
|
-
return sc;
|
|
82
|
-
}
|
|
83
|
-
return "Value";
|
|
84
|
-
}
|
|
85
|
-
default:
|
|
86
|
-
return "Value";
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
/** scalar TypeRef → PortFieldType (native go scalar kind). */
|
|
90
|
-
function goScalarPortField(scalar) {
|
|
91
|
-
return scalar === "string" ? "string" : scalar === "int" ? "int64" : scalar === "float" ? "float64" : "bool";
|
|
92
|
-
}
|
|
93
|
-
/** A static string-array port (every element a static string literal) — the graphddb `projection`
|
|
94
|
-
* shape. Returns the list of literal strings, or null when the node is not that shape. */
|
|
95
|
-
function staticStringArrElems(node) {
|
|
96
|
-
const arr = staticArrElems(node);
|
|
97
|
-
if (arr === null)
|
|
98
|
-
return null;
|
|
99
|
-
const out = [];
|
|
100
|
-
for (const el of arr) {
|
|
101
|
-
const e = classifyExpr(el);
|
|
102
|
-
if (e.kind !== "str")
|
|
103
|
-
return null;
|
|
104
|
-
out.push(e.value);
|
|
105
|
-
}
|
|
106
|
-
return out;
|
|
107
|
-
}
|
|
108
52
|
/**
|
|
109
|
-
* portFieldGoType — the NATIVE Go type for a covered port field (bc#90 / runtime-free)
|
|
110
|
-
* ports struct carries NO `dslcontracts.Value`
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
|
|
115
|
-
* Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no
|
|
116
|
-
* `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.
|
|
117
58
|
*/
|
|
118
|
-
function portFieldGoType(node,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// required-scalar inference below (which has no way to say "absent").
|
|
122
|
-
const optPort = optPortCompileG(node, inputPorts, where, plan);
|
|
123
|
-
if (optPort !== null)
|
|
124
|
-
return renderTypeRef(optPort.ref);
|
|
125
|
-
if (staticStringArrElems(node) !== null)
|
|
126
|
-
return "[]string";
|
|
127
|
-
// #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → []ElemT.
|
|
128
|
-
const arrElem = portArrayElemRef(node, inputPorts, plan);
|
|
129
|
-
if (arrElem !== null)
|
|
130
|
-
return `[]${renderTypeRef(arrElem)}`;
|
|
131
|
-
// #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) → []ElemT.
|
|
132
|
-
const priorArr = priorNodeArrElemInfo(node, typedNodes, plan);
|
|
133
|
-
if (priorArr !== null)
|
|
134
|
-
return `[]${renderTypeRef(priorArr.elemRef)}`;
|
|
135
|
-
const num = numLiteralGoExpr(node);
|
|
136
|
-
if (num !== null)
|
|
137
|
-
return Number.isInteger(node) ? "int64" : "float64";
|
|
138
|
-
// a ref to a `$as` element field (or an input map port) whose type is a MAP or NAMED struct (NOT a bare
|
|
139
|
-
// scalar) → the native map/struct type (#108-map: a `{map:V}` write port lowers to map[string]V).
|
|
140
|
-
const composite = portCompositeRef(node, inputPorts, asBinding, plan);
|
|
141
|
-
if (composite !== null)
|
|
142
|
-
return renderTypeRef(composite);
|
|
143
|
-
const ft = inferPortType(node, inputPorts, asBinding);
|
|
144
|
-
if (ft === "Value") {
|
|
145
|
-
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. ${GO_STATIC_PORT_FORMS} Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
|
|
146
|
-
}
|
|
147
|
-
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);
|
|
148
62
|
}
|
|
149
|
-
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
ref = goTypedInternals.typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan).ref;
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
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 };
|
|
163
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);
|
|
164
94
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (et !== undefined)
|
|
170
|
-
return { kind: "map", value: deriveTypeRef(et, plan) };
|
|
171
|
-
}
|
|
172
|
-
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}`);
|
|
173
99
|
}
|
|
174
|
-
if (ref === null)
|
|
175
|
-
return null;
|
|
176
|
-
if (ref.kind === "map" || ref.kind === "named")
|
|
177
|
-
return ref;
|
|
178
|
-
return null;
|
|
179
100
|
}
|
|
180
|
-
/**
|
|
181
|
-
*
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (asBinding !== undefined && elemBaseExpr !== undefined && e.path[0] === asBinding.name) {
|
|
188
|
-
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;
|
|
189
108
|
try {
|
|
190
|
-
|
|
109
|
+
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
|
|
191
110
|
}
|
|
192
111
|
catch {
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
if (acc.ref.kind !== "map" && acc.ref.kind !== "named")
|
|
196
|
-
return null;
|
|
197
|
-
return acc.expr;
|
|
198
|
-
}
|
|
199
|
-
// a WHOLE-port ref to an input MAP port (with elemType) — valid at top level OR inside a map node (a
|
|
200
|
-
// ref head that is NOT the `$as` element binding). Reads the native map[string]V off the input struct.
|
|
201
|
-
if ((asBinding === undefined || e.path[0] !== asBinding.name) && e.path.length === 1) {
|
|
202
|
-
const s = inputPorts[e.path[0]];
|
|
203
|
-
if (s !== undefined && s.type === "map" && s.elemType !== undefined) {
|
|
204
|
-
return `in.${goFieldName(e.path[0])}`;
|
|
112
|
+
/* untyped node — omit */
|
|
205
113
|
}
|
|
206
114
|
}
|
|
207
|
-
return
|
|
115
|
+
return typedNodes;
|
|
208
116
|
}
|
|
209
117
|
function staticArrElems(node) {
|
|
210
118
|
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
@@ -215,96 +123,16 @@ function staticArrElems(node) {
|
|
|
215
123
|
const arg = node.arr;
|
|
216
124
|
return Array.isArray(arg) ? arg : null;
|
|
217
125
|
}
|
|
218
|
-
/**
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
* on the read hot path. This REUSES the bc#108 `elemType` element lowering (deriveTypeRef over the input
|
|
225
|
-
* port's elemType), extended from the `map…over` element-source path to a plain componentRef port.
|
|
226
|
-
*
|
|
227
|
-
* A static string array is a DIFFERENT covered shape ([]string, staticStringArrElems) — handled ahead of
|
|
228
|
-
* this. An array port with NO resolvable elemType, an opt/multi-segment ref, or a non-array input schema
|
|
229
|
-
* resolves to null here → the caller keeps its existing fail-closed behaviour (never a silent box).
|
|
230
|
-
* Requires a TypePlan (an obj elemType maps to a named decl); without one it stays null (fail-closed).
|
|
231
|
-
*/
|
|
232
|
-
function portArrayElemRef(node, inputPorts, plan) {
|
|
233
|
-
if (plan === undefined)
|
|
234
|
-
return null;
|
|
235
|
-
if (staticStringArrElems(node) !== null)
|
|
236
|
-
return null; // the static-string-array port ([]string) — not this shape.
|
|
237
|
-
const e = classifyExpr(node);
|
|
238
|
-
if (e.kind !== "ref" || e.opt || e.path.length !== 1)
|
|
239
|
-
return null;
|
|
240
|
-
const schema = inputPorts?.[e.path[0]];
|
|
241
|
-
if (schema === undefined || (schema.type !== "array" && schema.type !== "arr"))
|
|
242
|
-
return null;
|
|
243
|
-
const et = schema.elemType;
|
|
244
|
-
if (et === undefined)
|
|
245
|
-
return null;
|
|
246
|
-
return deriveTypeRef(et, plan);
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* isNativeRawComponent — a component covered by the combined read emitter. Strictly sequential
|
|
250
|
-
* (no real concurrency), body is ALL componentRef (no map/cond), default 'fail'/'continue'
|
|
251
|
-
* policy, relationKind is single/connection or absent, and EVERY node carries outType + the
|
|
252
|
-
* component carries outputType. relationKind:single / bindField ARE the point — inline exec makes
|
|
253
|
-
* the child-present decision from the real parent result. Ports AND the output are evaluated via
|
|
254
|
-
* the primitive SSoT over a Value-space portScope (so a dynamic operator in a port or an
|
|
255
|
-
* obj-assembly output is fine — the SSoT handles fail-closed semantics; only the RESULT plane +
|
|
256
|
-
* PORT container go native). A covered SHAPE that lacks outType is a fail-closed error raised at
|
|
257
|
-
* emit (see assertTypedCoverage) — never a silent re-box.
|
|
258
|
-
*/
|
|
259
|
-
/**
|
|
260
|
-
* numLiteralGoExpr — a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
|
|
261
|
-
* `classifyExpr` deliberately leaves a bare number `dynamic` (its checked int/float range rules are
|
|
262
|
-
* the evaluate SSoT, expr.go evalNumberLiteral), so a plain number never reaches the str/bool/null
|
|
263
|
-
* static branch. But a number literal IS statically resolvable to a native Go Value: the SAME
|
|
264
|
-
* range check the runtime applies is performed HERE at generation time (integral literal in the
|
|
265
|
-
* safe 2^53-1 window → int64; a fractional/exponent literal → the checked float64). Returns the Go
|
|
266
|
-
* expression for the literal's Value, or null when the node is not a bare number OR the integral
|
|
267
|
-
* literal is out of the safe window (that out-of-range case keeps the runtime INVALID_LITERAL
|
|
268
|
-
* semantics — the port is NOT covered natively, so the whole component falls through, never a
|
|
269
|
-
* silently-wrong inline). Mirrors coderuntime.evalNumberLiteral exactly.
|
|
270
|
-
*/
|
|
271
|
-
const NUM_SAFE_INT = 9007199254740991; // 2^53 - 1 (expr.go safeInt)
|
|
272
|
-
function numLiteralGoExpr(node) {
|
|
273
|
-
if (typeof node !== "number" || !Number.isFinite(node))
|
|
274
|
-
return null;
|
|
275
|
-
if (Number.isInteger(node)) {
|
|
276
|
-
if (node < -NUM_SAFE_INT || node > NUM_SAFE_INT)
|
|
277
|
-
return null; // out of safe window → not covered (runtime INVALID_LITERAL)
|
|
278
|
-
return `${PKG}.Value(int64(${node}))`;
|
|
279
|
-
}
|
|
280
|
-
// fractional / exponent literal → checked float64 (finite, already guaranteed above).
|
|
281
|
-
return `${PKG}.Value(float64(${node}))`;
|
|
282
|
-
}
|
|
283
|
-
/** A port node is static (literal / ref / concat of those / static arr / bare number literal / an
|
|
284
|
-
* optional-input read) — resolvable to a native Go value with no NewObj / no evaluate interpreter. This
|
|
285
|
-
* is the SHAPE gate that decides native ELIGIBILITY; the lowering itself (portFieldGoType / emitPortInit)
|
|
286
|
-
* is what proves each shape, and fails closed loudly on one it cannot lower. */
|
|
287
|
-
function portIsStatic(node, inputPorts) {
|
|
288
|
-
const arr = staticArrElems(node);
|
|
289
|
-
if (arr !== null)
|
|
290
|
-
return arr.every((el) => portIsStatic(el, inputPorts));
|
|
291
|
-
if (numLiteralGoExpr(node) !== null)
|
|
292
|
-
return true; // bare number literal (e.g. Query `limit`)
|
|
293
|
-
// An optional-input read (`opt($.x)` / `coalesce(opt($.x), 20)` / `ne(opt($.x), null)`) is a
|
|
294
|
-
// statically-resolvable port — the opt lane lowers it (or fails closed with the specific reason).
|
|
295
|
-
if (inputPorts !== undefined && exprReadsOptionalInput(node, inputPorts))
|
|
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);
|
|
296
132
|
return true;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
case "bool":
|
|
301
|
-
case "null":
|
|
302
|
-
case "ref":
|
|
303
|
-
return true;
|
|
304
|
-
case "concat":
|
|
305
|
-
return e.parts.every((p) => portIsStatic(reconstructG(p), inputPorts));
|
|
306
|
-
default:
|
|
307
|
-
return false;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false; // an eligibility probe: any lowering failure ⇒ not native-covered here.
|
|
308
136
|
}
|
|
309
137
|
}
|
|
310
138
|
/** The output must lower to a typed struct/value with no Value-scope read: a ref whose head is a
|
|
@@ -352,8 +180,8 @@ function isFullyTyped(comp) {
|
|
|
352
180
|
}
|
|
353
181
|
return true;
|
|
354
182
|
}
|
|
355
|
-
function isNativeRaw(comp) {
|
|
356
|
-
return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
|
|
183
|
+
function isNativeRaw(comp, plan) {
|
|
184
|
+
return isSequentialComponentRefRead(comp, plan) && isFullyTyped(comp);
|
|
357
185
|
}
|
|
358
186
|
/**
|
|
359
187
|
* coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet). A BATCHED `map` node whose:
|
|
@@ -368,7 +196,7 @@ function isNativeRaw(comp) {
|
|
|
368
196
|
* element struct by copying the over element's typed fields + materializing the into field from the
|
|
369
197
|
* aligned batch raw — NO boxed Value on any plane.
|
|
370
198
|
*/
|
|
371
|
-
function coveredMapNode(n, bodyIds, comp) {
|
|
199
|
+
function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
|
|
372
200
|
if (n === null || typeof n !== "object" || !("map" in n))
|
|
373
201
|
return false;
|
|
374
202
|
const m = n.map;
|
|
@@ -405,11 +233,22 @@ function coveredMapNode(n, bodyIds, comp) {
|
|
|
405
233
|
return false;
|
|
406
234
|
}
|
|
407
235
|
const ports = m.ports;
|
|
236
|
+
const asBinding = mapAsBinding(comp, n, typedNodes, plan);
|
|
408
237
|
for (const p of Object.keys(ports))
|
|
409
|
-
if (!portIsStatic(ports[p], comp
|
|
238
|
+
if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
|
|
410
239
|
return false;
|
|
411
240
|
return true;
|
|
412
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
|
+
}
|
|
413
252
|
/**
|
|
414
253
|
* coveredFanoutNode (v3) — a FANOUT node (first-class `@refs` id-list fan-out) is covered when:
|
|
415
254
|
* - over is a static ref to a PRIOR BODY NODE's typed arr field OR an INPUT array port with elemType,
|
|
@@ -418,7 +257,7 @@ function coveredMapNode(n, bodyIds, comp) {
|
|
|
418
257
|
* The fanout de-boxes to: one deduped batched dispatch → first-seen dedupe (by the elem's dedupeKey
|
|
419
258
|
* field) + dangling drop + implicitSource strip + connection wrap {items, cursor:nil} — NO boxed Value.
|
|
420
259
|
*/
|
|
421
|
-
function coveredFanoutNode(n, bodyIds, comp) {
|
|
260
|
+
function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
|
|
422
261
|
if (n === null || typeof n !== "object" || !("fanout" in n))
|
|
423
262
|
return false;
|
|
424
263
|
const f = n.fanout;
|
|
@@ -438,8 +277,15 @@ function coveredFanoutNode(n, bodyIds, comp) {
|
|
|
438
277
|
return false;
|
|
439
278
|
}
|
|
440
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
|
+
}
|
|
441
287
|
for (const p of Object.keys(ports))
|
|
442
|
-
if (!portIsStatic(ports[p], comp
|
|
288
|
+
if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
|
|
443
289
|
return false;
|
|
444
290
|
return true;
|
|
445
291
|
}
|
|
@@ -448,8 +294,8 @@ function coveredFanoutNode(n, bodyIds, comp) {
|
|
|
448
294
|
* reads. A real-concurrency plan whose parallel members include a map / bindField-relation child is
|
|
449
295
|
* NOT covered here (falls through) — the static-parallel orchestration covers the componentRef fan-out
|
|
450
296
|
* (the GroupAccessRead shape); a parallel map/relation-child stage is a further shape (out of scope). */
|
|
451
|
-
function isSequentialComponentRefRead(comp) {
|
|
452
|
-
return coverageRejectReason(comp) === null;
|
|
297
|
+
function isSequentialComponentRefRead(comp, plan) {
|
|
298
|
+
return coverageRejectReason(comp, plan) === null;
|
|
453
299
|
}
|
|
454
300
|
/**
|
|
455
301
|
* coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89). Returns
|
|
@@ -459,10 +305,11 @@ function isSequentialComponentRefRead(comp) {
|
|
|
459
305
|
* never disagree. This is the message the fail-closed dispatch surfaces per non-native component — the
|
|
460
306
|
* user's signal to add native coverage, fix a genuinely-dynamic spec, or use the --ir dynamic surface.
|
|
461
307
|
*/
|
|
462
|
-
function coverageRejectReason(comp) {
|
|
308
|
+
function coverageRejectReason(comp, plan) {
|
|
463
309
|
const ep = execPlan(comp);
|
|
464
310
|
if (ep === null)
|
|
465
311
|
return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
|
|
312
|
+
const typedNodes = buildTypedNodes(comp, plan);
|
|
466
313
|
// A parallel-stage member must be a componentRef node (no map) so the static parallel orchestration
|
|
467
314
|
// is a clean fan-out — the bounded-concurrency error-precedence protocol is proven for that shape.
|
|
468
315
|
// A map child inside a parallel stage still falls through (its per-element loop is a further shape).
|
|
@@ -507,7 +354,7 @@ function coverageRejectReason(comp) {
|
|
|
507
354
|
if ("fanout" in n) {
|
|
508
355
|
// v3: a first-class fanout node (deduped connection list-in fan-out) IS covered when its over is a
|
|
509
356
|
// covered ref (prior-node arr / input array with elemType) and its element ports are static.
|
|
510
|
-
if (!coveredFanoutNode(n, bodyIds, comp))
|
|
357
|
+
if (!coveredFanoutNode(n, bodyIds, comp, plan, typedNodes))
|
|
511
358
|
return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
|
|
512
359
|
continue;
|
|
513
360
|
}
|
|
@@ -515,7 +362,7 @@ function coverageRejectReason(comp) {
|
|
|
515
362
|
// #86 pt2 batched map...into / #93 shape 2/3 / #108 guarded map (no into) / map-over-input (with
|
|
516
363
|
// elemType) ARE covered. A non-covered map shape (guard+into heterogeneous / over-input without
|
|
517
364
|
// elemType) falls through to the loud fail-closed reject.
|
|
518
|
-
if (!coveredMapNode(n, bodyIds, comp))
|
|
365
|
+
if (!coveredMapNode(n, bodyIds, comp, plan, typedNodes))
|
|
519
366
|
return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
|
|
520
367
|
continue;
|
|
521
368
|
}
|
|
@@ -546,7 +393,7 @@ function coverageRejectReason(comp) {
|
|
|
546
393
|
if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
547
394
|
return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
|
|
548
395
|
for (const p of Object.keys(cr.ports))
|
|
549
|
-
if (portIsStatic(cr.ports[p], comp
|
|
396
|
+
if (portIsStatic(cr.ports[p], comp, plan, typedNodes) === false)
|
|
550
397
|
return `node '${n.id}' port '${p}' is not statically resolvable`;
|
|
551
398
|
}
|
|
552
399
|
if (!outputIsNativeLowerable(comp))
|
|
@@ -554,21 +401,21 @@ function coverageRejectReason(comp) {
|
|
|
554
401
|
return null;
|
|
555
402
|
}
|
|
556
403
|
/** Fail closed if a covered read SHAPE lacks the typed annotations (no silent boxed re-box). */
|
|
557
|
-
function assertTypedCoverage(comp) {
|
|
558
|
-
if (!isSequentialComponentRefRead(comp))
|
|
404
|
+
function assertTypedCoverage(comp, plan) {
|
|
405
|
+
if (!isSequentialComponentRefRead(comp, plan))
|
|
559
406
|
return;
|
|
560
407
|
if (isFullyTyped(comp))
|
|
561
408
|
return;
|
|
562
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).`);
|
|
563
410
|
}
|
|
564
411
|
// ── native ports struct + PortReader ──────────────────────────────────────────────────
|
|
565
|
-
function emitPortsStruct(comp, node,
|
|
412
|
+
function emitPortsStruct(comp, node, plan, typedNodes, asBinding) {
|
|
566
413
|
const structName = portsStructName(comp.name, node.id);
|
|
567
414
|
const portNames = Object.keys(node.ports);
|
|
568
415
|
if (portNames.length === 0) {
|
|
569
416
|
return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
|
|
570
417
|
}
|
|
571
|
-
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));
|
|
572
419
|
const fieldNames = portNames.map(goPortFieldName);
|
|
573
420
|
const fields = portNames
|
|
574
421
|
.map((p, i) => `\t${fieldNames[i]} ${fieldTypes[i]} // ${JSON.stringify(p)}`)
|
|
@@ -782,7 +629,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
|
|
|
782
629
|
// struct element). A pure-static/concat port ignores the binding.
|
|
783
630
|
const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
784
631
|
const asBinding = { name: m.as, ref: elemRef, plan };
|
|
785
|
-
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);
|
|
786
633
|
const batchStruct = `${structName}_batch`;
|
|
787
634
|
return `${elemStruct}
|
|
788
635
|
|
|
@@ -800,7 +647,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
|
|
|
800
647
|
const structName = portsStructName(comp.name, node.id);
|
|
801
648
|
const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
|
|
802
649
|
const asBinding = { name: f.as, ref: elemRef, plan };
|
|
803
|
-
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);
|
|
804
651
|
const batchStruct = `${structName}_batch`;
|
|
805
652
|
return `${elemStruct}
|
|
806
653
|
|
|
@@ -947,43 +794,6 @@ function mapOverElemInfo(comp, node, typedNodes, plan) {
|
|
|
947
794
|
function mapOverElemType(comp, node, typedNodes, plan) {
|
|
948
795
|
return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
|
|
949
796
|
}
|
|
950
|
-
/**
|
|
951
|
-
* priorNodeArrElemInfo (#129) — resolve a leaf port that is a `ref` into a PRIOR BODY NODE whose
|
|
952
|
-
* annotated `outType` is (or reaches, through a field path) a native array, to its element TypeRef +
|
|
953
|
-
* the native Go slice expression that yields it. This GENERALIZES the `map…over` prior-node-arr
|
|
954
|
-
* dataflow (mapOverElemInfo's first branch) to ANY leaf input port: a fold/dedup leaf whose `items`
|
|
955
|
-
* port is `{ref:["<priorNode>"]}` (or `.items` of a prior connection node) now lowers to `[]ElemT`
|
|
956
|
-
* fed straight from the prior node's typed local — ZERO boxed Value on the read hot path.
|
|
957
|
-
*
|
|
958
|
-
* The element type comes STRICTLY from the prior node's EXPLICIT `outType` annotation walked through
|
|
959
|
-
* typedFieldAccess (BC does NOT infer — consumer-interface C3 / [[graphddb-typed-native-static-link]]):
|
|
960
|
-
* if the head is not a prior body node, or its outType (through the field path) is not an `arr`, this
|
|
961
|
-
* returns null and the caller keeps its existing fail-closed behaviour (never a silent box). A
|
|
962
|
-
* single-segment INPUT-array port (elemType) is a DIFFERENT covered shape (portArrayElemRef) — handled
|
|
963
|
-
* ahead of this. Requires the typedNodes map (prior-node outType TypeRefs) and a TypePlan.
|
|
964
|
-
*/
|
|
965
|
-
function priorNodeArrElemInfo(node, typedNodes, plan) {
|
|
966
|
-
if (typedNodes === undefined || plan === undefined)
|
|
967
|
-
return null;
|
|
968
|
-
if (staticArrElems(node) !== null)
|
|
969
|
-
return null; // a static array literal — not a prior-node ref.
|
|
970
|
-
const e = classifyExpr(node);
|
|
971
|
-
if (e.kind !== "ref" || e.opt || e.path.length < 1)
|
|
972
|
-
return null;
|
|
973
|
-
if (!typedNodes.has(e.path[0]))
|
|
974
|
-
return null; // head is not a prior body node → not this shape.
|
|
975
|
-
const baseRef = typedNodes.get(e.path[0]);
|
|
976
|
-
let acc;
|
|
977
|
-
try {
|
|
978
|
-
acc = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
|
|
979
|
-
}
|
|
980
|
-
catch {
|
|
981
|
-
return null; // the field path does not resolve on the prior node's outType → fail-closed at caller.
|
|
982
|
-
}
|
|
983
|
-
if (acc.ref.kind !== "arr")
|
|
984
|
-
return null; // the prior-node result (through the path) is not an array.
|
|
985
|
-
return { elemRef: acc.ref.elem, overExpr: acc.expr };
|
|
986
|
-
}
|
|
987
797
|
function classifyPort(node) {
|
|
988
798
|
if (staticArrElems(node) !== null)
|
|
989
799
|
return "static";
|
|
@@ -1178,124 +988,18 @@ function emitInDecoder(comp, plan) {
|
|
|
1178
988
|
lines.push(`}`);
|
|
1179
989
|
return lines.join("\n");
|
|
1180
990
|
}
|
|
1181
|
-
/** The port forms the STATIC (non-optional) lane lowers — named in its reject message so the message
|
|
1182
|
-
* describes what is actually covered rather than a stale corpus list. */
|
|
1183
|
-
const GO_STATIC_PORT_FORMS = "This port does not read an optional input, so it lowers through the STATIC port forms: a string / bool / number literal, a static string-array (projection), a ref to a REQUIRED scalar input port, a ref to an input array/map port or a prior node's array (declared elemType), a ref to a `$as` element field, or a concat of native strings. A dynamic operator at port position is not covered on this lane. (A port that DOES read an optional input lowers through the full native expression compiler, which admits the whole closed operator set — so the covered operator set at port position currently depends on the lane, not on the operator.)";
|
|
1184
991
|
/**
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
* `
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
* 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.
|
|
1191
997
|
*/
|
|
1192
|
-
function
|
|
1193
|
-
|
|
1194
|
-
if (
|
|
1195
|
-
return
|
|
1196
|
-
|
|
1197
|
-
switch (e.kind) {
|
|
1198
|
-
case "str":
|
|
1199
|
-
return want === "string" ? JSON.stringify(e.value) : null;
|
|
1200
|
-
case "bool":
|
|
1201
|
-
return want === "bool" ? (e.value ? "true" : "false") : null;
|
|
1202
|
-
case "null":
|
|
1203
|
-
return null; // a null literal is a Value(nil), never a required native scalar
|
|
1204
|
-
case "ref": {
|
|
1205
|
-
const sc = resolveRef(e.path[0], e.path.slice(1), e.opt);
|
|
1206
|
-
if (sc === null || sc.scalar !== want)
|
|
1207
|
-
return null;
|
|
1208
|
-
return sc.expr;
|
|
1209
|
-
}
|
|
1210
|
-
case "concat": {
|
|
1211
|
-
if (want !== "string")
|
|
1212
|
-
return null;
|
|
1213
|
-
const parts = [];
|
|
1214
|
-
for (const p of e.parts) {
|
|
1215
|
-
const sub = emitNativeScalar(reconstructG(p), "string", resolveRef);
|
|
1216
|
-
if (sub === null)
|
|
1217
|
-
return null;
|
|
1218
|
-
// parenthesize a `+` sub-expression so string concatenation binds correctly (a ref/literal is
|
|
1219
|
-
// atomic; a nested concat is already a single `a + b` — wrap to keep it one operand).
|
|
1220
|
-
parts.push(sub.includes(" + ") ? `(${sub})` : sub);
|
|
1221
|
-
}
|
|
1222
|
-
return parts.join(" + ");
|
|
1223
|
-
}
|
|
1224
|
-
default:
|
|
1225
|
-
return null;
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
/**
|
|
1229
|
-
* emitPortInit — emit the port field build for one port as a FULLY NATIVE Go expression (bc#90 /
|
|
1230
|
-
* runtime-free). Every covered port lowers to a concrete Go value assigned straight into the ports
|
|
1231
|
-
* struct field — NO `dslcontracts.Value`, NO nvVE helper, NO `.(T)` type-assert:
|
|
1232
|
-
* - a static string array (projection) → `[]string{"a", "b", ...}` (change #2);
|
|
1233
|
-
* - a bare int/float number literal (e.g. Query `limit: 20`) → `int64(20)` / `float64(...)`;
|
|
1234
|
-
* - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
|
|
1235
|
-
* A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered
|
|
1236
|
-
* plane — a boxed escape would re-introduce the bc-runtime coupling this module removes). Returns the
|
|
1237
|
-
* struct field initializer (`Field: <expr>`). `resolveRef` resolves a ref head for the scalar path.
|
|
1238
|
-
*/
|
|
1239
|
-
function emitPortInit(pn, expr, fieldGoType, resolveRef, inputPorts, plan, typedNodes, asBinding, elemBaseExpr) {
|
|
1240
|
-
const fieldName = goPortFieldName(pn);
|
|
1241
|
-
// The OPTIONAL-input lane (*T pass-through / coalesce default) — same shared compile as
|
|
1242
|
-
// portFieldGoType, so the field type and its initializer are derived from ONE lowering.
|
|
1243
|
-
const optPort = optPortCompileG(expr, inputPorts ?? {}, `port '${pn}'`, plan);
|
|
1244
|
-
if (optPort !== null)
|
|
1245
|
-
return `${fieldName}: ${optPort.expr}`;
|
|
1246
|
-
// #108-map: a port that is a ref to a `$as` element MAP/NAMED field (or an input map port) → the typed
|
|
1247
|
-
// composite value directly off the element/input (native map[string]V / struct — ZERO boxed Value).
|
|
1248
|
-
if (plan !== undefined) {
|
|
1249
|
-
const compExpr = emitCompositePortValue(expr, inputPorts ?? {}, asBinding, plan, elemBaseExpr);
|
|
1250
|
-
if (compExpr !== null)
|
|
1251
|
-
return `${fieldName}: ${compExpr}`;
|
|
1252
|
-
}
|
|
1253
|
-
// #110: a componentRef port bound to a runtime array input port (with elemType) → the native []ElemT,
|
|
1254
|
-
// read straight from the concrete input struct (`in.<Field>`) — ZERO boxed Value on the read hot path.
|
|
1255
|
-
// The over-elem/element-source case (map…over) already resolves via mapOverElemInfo; this is the plain
|
|
1256
|
-
// componentRef port (an IN-list / array-bound WHERE head).
|
|
1257
|
-
if (inputPorts !== undefined && portArrayElemRef(expr, inputPorts, plan) !== null) {
|
|
1258
|
-
const e = classifyExpr(expr);
|
|
1259
|
-
if (e.kind === "ref" && e.path.length === 1) {
|
|
1260
|
-
return `${fieldName}: in.${goFieldName(e.path[0])}`;
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
// #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) →
|
|
1264
|
-
// the native []ElemT read straight off the prior node's typed local (`t_<node>.Field…`) — ZERO boxed
|
|
1265
|
-
// Value on the read hot path. The over-source case (map…over) resolves via mapOverElemInfo; this is
|
|
1266
|
-
// the plain leaf array input port (a fold/dedup leaf whose `items` = a prior node's array result).
|
|
1267
|
-
const priorArr = priorNodeArrElemInfo(expr, typedNodes, plan);
|
|
1268
|
-
if (priorArr !== null)
|
|
1269
|
-
return `${fieldName}: ${priorArr.overExpr}`;
|
|
1270
|
-
// change #2: a static string array (projection) → a native `[]string{...}` literal.
|
|
1271
|
-
const strArr = staticStringArrElems(expr);
|
|
1272
|
-
if (strArr !== null) {
|
|
1273
|
-
if (fieldGoType !== "[]string") {
|
|
1274
|
-
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).`);
|
|
1275
|
-
}
|
|
1276
|
-
return `${fieldName}: []string{${strArr.map((s) => JSON.stringify(s)).join(", ")}}`;
|
|
1277
|
-
}
|
|
1278
|
-
// a bare number literal (e.g. Query `limit: 20`): the SSoT range check ran in numLiteralGoExpr —
|
|
1279
|
-
// emit the concrete native int64/float64 (unwrap the dslcontracts.Value(...) box: it was only there
|
|
1280
|
-
// for the old boxed path; the range check is what matters, and it already passed).
|
|
1281
|
-
const num = numLiteralGoExpr(expr);
|
|
1282
|
-
if (num !== null) {
|
|
1283
|
-
if (fieldGoType === "int64")
|
|
1284
|
-
return `${fieldName}: int64(${expr})`;
|
|
1285
|
-
if (fieldGoType === "float64")
|
|
1286
|
-
return `${fieldName}: float64(${expr})`;
|
|
1287
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90): port '${pn}' is a number literal but its field type is '${fieldGoType}'.`);
|
|
1288
|
-
}
|
|
1289
|
-
// string / bool / scalar-input / concat → native scalar expression (no Value, no `.(T)`).
|
|
1290
|
-
const scalarWant = fieldGoType;
|
|
1291
|
-
const native = emitNativeScalar(expr, scalarWant, resolveRef);
|
|
1292
|
-
if (native !== null) {
|
|
1293
|
-
return `${fieldName}: ${native}`;
|
|
1294
|
-
}
|
|
1295
|
-
// FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would
|
|
1296
|
-
// re-couple the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the
|
|
1297
|
-
// component is not native-eligible and must regenerate on the boxed straight-line path.
|
|
1298
|
-
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. ${GO_STATIC_PORT_FORMS} 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}`;
|
|
1299
1003
|
}
|
|
1300
1004
|
/**
|
|
1301
1005
|
* emitParallelStageArm (bc#87) — EXPLICIT static parallel orchestration for a real-concurrency stage.
|
|
@@ -1318,7 +1022,7 @@ function emitPortInit(pn, expr, fieldGoType, resolveRef, inputPorts, plan, typed
|
|
|
1318
1022
|
*
|
|
1319
1023
|
* NO RunPlan / []OpSpec / planSpec walk: the staging, the bound, and which ops are parallel are baked.
|
|
1320
1024
|
*/
|
|
1321
|
-
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) {
|
|
1322
1026
|
const lines = [];
|
|
1323
1027
|
const ids = stage.map((i) => comp.body[i].id);
|
|
1324
1028
|
lines.push(`\t// ── bc#87 real-concurrency stage {${ids.map((x) => JSON.stringify(x)).join(", ")}} — static parallel`);
|
|
@@ -1407,11 +1111,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1407
1111
|
}
|
|
1408
1112
|
}
|
|
1409
1113
|
const inits = [];
|
|
1410
|
-
const resolveRef = resolveRefAt(atPos);
|
|
1411
1114
|
for (const pn of Object.keys(node.ports)) {
|
|
1412
|
-
|
|
1413
|
-
const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
|
|
1414
|
-
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)));
|
|
1415
1116
|
}
|
|
1416
1117
|
lines.push(`${ind}ps_${s} = ${structName}{${inits.join(", ")}}`);
|
|
1417
1118
|
lines.push(`${ind}run_${s} = true`);
|
|
@@ -1518,35 +1219,6 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1518
1219
|
// assert on a row value — the runner reads row.<Field> directly and copies each into the outType local.
|
|
1519
1220
|
lines.push(`func ${runnerName(comp.name)}[H ${handlerIfaceName(comp.name)}](handlers H, in ${inStructName(comp.name)}) (${outStructType}, error) {`);
|
|
1520
1221
|
lines.push(`\t_ = in`);
|
|
1521
|
-
// resolveNativeRef for this runner: a ref head is either a PRIOR NODE (read its typed struct field
|
|
1522
|
-
// directly) or an INPUT PORT (read the typed `in.<Field>`). A REQUIRED scalar resolves to a native
|
|
1523
|
-
// Go expression; an opt / non-scalar / refOpt resolves to null (falls back to the nvVE Value path).
|
|
1524
|
-
const resolveRefAt = (atPos) => (head, restPath, opt) => {
|
|
1525
|
-
if (opt)
|
|
1526
|
-
return null; // a refOpt may be nil at runtime — keep the Value path (opt-nil semantics)
|
|
1527
|
-
if (priorNodeAt(head, atPos)) {
|
|
1528
|
-
const baseRef = typedNodes.get(head);
|
|
1529
|
-
let acc;
|
|
1530
|
-
let expr;
|
|
1531
|
-
try {
|
|
1532
|
-
const r = goTypedInternals.typedFieldAccess(typedLocal(head), baseRef, restPath, plan);
|
|
1533
|
-
expr = r.expr;
|
|
1534
|
-
acc = r.ref;
|
|
1535
|
-
}
|
|
1536
|
-
catch {
|
|
1537
|
-
return null;
|
|
1538
|
-
}
|
|
1539
|
-
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1540
|
-
return null;
|
|
1541
|
-
return { expr, scalar: goScalar(acc.scalar) };
|
|
1542
|
-
}
|
|
1543
|
-
if (restPath.length !== 0)
|
|
1544
|
-
return null; // input.<field> path is a Value walk — not a native scalar
|
|
1545
|
-
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1546
|
-
if (scalar === undefined)
|
|
1547
|
-
return null;
|
|
1548
|
-
return { expr: `in.${goFieldName(head)}`, scalar };
|
|
1549
|
-
};
|
|
1550
1222
|
for (const k of order.keys()) {
|
|
1551
1223
|
const node = comp.body[order[k]];
|
|
1552
1224
|
const id = node.id;
|
|
@@ -1569,7 +1241,7 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1569
1241
|
// ascending-sorted stages), so we emit the block at the first member and skip the rest.
|
|
1570
1242
|
const stage = ep.parallelStageOf.get(i);
|
|
1571
1243
|
if (stage !== undefined && stage[0] === i) {
|
|
1572
|
-
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));
|
|
1573
1245
|
k += stage.length - 1;
|
|
1574
1246
|
continue;
|
|
1575
1247
|
}
|
|
@@ -1641,11 +1313,8 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1641
1313
|
// dslcontracts.Value, ZERO nvVE helper, ZERO `.(T)`. A genuinely-dynamic port FAILS CLOSED.
|
|
1642
1314
|
const inits = [];
|
|
1643
1315
|
const boundArg = node.bindField !== undefined ? `bound_${s}` : "nil";
|
|
1644
|
-
const resolveRef = resolveRefAt(k);
|
|
1645
1316
|
for (const pn of portNames) {
|
|
1646
|
-
|
|
1647
|
-
const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
|
|
1648
|
-
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)));
|
|
1649
1318
|
}
|
|
1650
1319
|
lines.push(`${indent}ports_${s} := ${structName}{${inits.join(", ")}}`);
|
|
1651
1320
|
const oc = `row_${s}`;
|
|
@@ -1738,67 +1407,15 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1738
1407
|
const mkGuard = (elemVar) => {
|
|
1739
1408
|
if (!hasGuard)
|
|
1740
1409
|
return null;
|
|
1741
|
-
const resolveHead = (
|
|
1742
|
-
|
|
1743
|
-
return { expr: elemVar, ref: overElemRef };
|
|
1744
|
-
if (priorHere(head))
|
|
1745
|
-
return { expr: typedLocal(head), ref: typedNodes.get(head) };
|
|
1746
|
-
return goInputHeadRef(head, comp, plan);
|
|
1747
|
-
};
|
|
1748
|
-
const be = makeGoExprBackend(zeroOut, resolveHead, plan);
|
|
1749
|
-
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);
|
|
1750
1412
|
};
|
|
1751
1413
|
// emit the per-element native ports struct build (shared; `il` is the indent inside the element
|
|
1752
1414
|
// loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
|
|
1753
1415
|
const buildPorts = (il, elemVar) => {
|
|
1754
|
-
const out = [];
|
|
1755
|
-
const inits = [];
|
|
1756
|
-
// resolveNativeRef for a map element port: a ref head is the element binding ($as → the over element
|
|
1757
|
-
// typed struct field `elemVar.<Field>`), a PRIOR NODE (its typed struct field), or an INPUT PORT
|
|
1758
|
-
// (`in.<Field>`). A REQUIRED scalar lowers to a native Go expr; opt / non-scalar → null (Value path).
|
|
1759
|
-
const resolveRef = (head, restPath, opt) => {
|
|
1760
|
-
if (opt)
|
|
1761
|
-
return null;
|
|
1762
|
-
let baseExpr;
|
|
1763
|
-
let baseRef;
|
|
1764
|
-
if (head === asName) {
|
|
1765
|
-
baseExpr = elemVar;
|
|
1766
|
-
baseRef = overElemRef;
|
|
1767
|
-
}
|
|
1768
|
-
else if (priorHere(head)) {
|
|
1769
|
-
baseExpr = typedLocal(head);
|
|
1770
|
-
baseRef = typedNodes.get(head);
|
|
1771
|
-
}
|
|
1772
|
-
else {
|
|
1773
|
-
if (restPath.length !== 0)
|
|
1774
|
-
return null;
|
|
1775
|
-
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1776
|
-
if (scalar === undefined)
|
|
1777
|
-
return null;
|
|
1778
|
-
return { expr: `in.${goFieldName(head)}`, scalar };
|
|
1779
|
-
}
|
|
1780
|
-
let acc;
|
|
1781
|
-
let expr;
|
|
1782
|
-
try {
|
|
1783
|
-
const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
|
|
1784
|
-
expr = r.expr;
|
|
1785
|
-
acc = r.ref;
|
|
1786
|
-
}
|
|
1787
|
-
catch {
|
|
1788
|
-
return null;
|
|
1789
|
-
}
|
|
1790
|
-
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1791
|
-
return null;
|
|
1792
|
-
return { expr, scalar: goScalar(acc.scalar) };
|
|
1793
|
-
};
|
|
1794
1416
|
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
// #108-map: pass the element binding + base expr so a `$as` MAP/NAMED field port reads the typed composite.
|
|
1798
|
-
inits.push(emitPortInit(pn, m.ports[pn], fieldGoType, resolveRef, comp.inputPorts, plan, undefined, asBinding, elemLocal(s)));
|
|
1799
|
-
}
|
|
1800
|
-
out.push(`${il}ep_${s} := ${structName}{${inits.join(", ")}}`);
|
|
1801
|
-
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(", ")}}`];
|
|
1802
1419
|
};
|
|
1803
1420
|
const lines = [];
|
|
1804
1421
|
const closers = [];
|
|
@@ -1936,47 +1553,8 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
|
|
|
1936
1553
|
const dkGo = goFieldName(f.dedupeKey);
|
|
1937
1554
|
// per-id native ports build (same as a batched map's per-element ports). $as → the over id value.
|
|
1938
1555
|
const buildPorts = (il, elemVar) => {
|
|
1939
|
-
const inits = [];
|
|
1940
|
-
const resolveRef = (head, restPath, opt) => {
|
|
1941
|
-
if (opt)
|
|
1942
|
-
return null;
|
|
1943
|
-
let baseExpr;
|
|
1944
|
-
let baseRef;
|
|
1945
|
-
if (head === asName) {
|
|
1946
|
-
baseExpr = elemVar;
|
|
1947
|
-
baseRef = overElemRef;
|
|
1948
|
-
}
|
|
1949
|
-
else if (priorHere(head)) {
|
|
1950
|
-
baseExpr = typedLocal(head);
|
|
1951
|
-
baseRef = typedNodes.get(head);
|
|
1952
|
-
}
|
|
1953
|
-
else {
|
|
1954
|
-
if (restPath.length !== 0)
|
|
1955
|
-
return null;
|
|
1956
|
-
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1957
|
-
if (scalar === undefined)
|
|
1958
|
-
return null;
|
|
1959
|
-
return { expr: `in.${goFieldName(head)}`, scalar };
|
|
1960
|
-
}
|
|
1961
|
-
let acc;
|
|
1962
|
-
let expr;
|
|
1963
|
-
try {
|
|
1964
|
-
const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
|
|
1965
|
-
expr = r.expr;
|
|
1966
|
-
acc = r.ref;
|
|
1967
|
-
}
|
|
1968
|
-
catch {
|
|
1969
|
-
return null;
|
|
1970
|
-
}
|
|
1971
|
-
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1972
|
-
return null;
|
|
1973
|
-
return { expr, scalar: goScalar(acc.scalar) };
|
|
1974
|
-
};
|
|
1975
1556
|
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1976
|
-
|
|
1977
|
-
const fieldGoType = portFieldGoType(f.ports[pn], comp.inputPorts, `fanout port '${pn}' of node '${node.id}'`, asBinding, plan);
|
|
1978
|
-
inits.push(emitPortInit(pn, f.ports[pn], fieldGoType, resolveRef, comp.inputPorts, plan));
|
|
1979
|
-
}
|
|
1557
|
+
const inits = portNames.map((pn) => emitPortInit(pn, f.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: elemVar }));
|
|
1980
1558
|
return [`${il}ep_${s} := ${structName}{${inits.join(", ")}}`];
|
|
1981
1559
|
};
|
|
1982
1560
|
const lines = [];
|
|
@@ -2045,59 +1623,6 @@ function goInputHeadRef(head, comp, plan) {
|
|
|
2045
1623
|
return null;
|
|
2046
1624
|
return { expr: `in.${goFieldName(head)}`, ref };
|
|
2047
1625
|
}
|
|
2048
|
-
/**
|
|
2049
|
-
* optPortCompileG — the OPTIONAL-input port lane (go). The rust twin's mirror: a component may
|
|
2050
|
-
* declare an input port `{opt:T}` (PortSchema `required:false`), and the only native representation of
|
|
2051
|
-
* "the value is absent" is `*T`, so such a port never lowers through the required-scalar inference. The
|
|
2052
|
-
* port expression compiles with the SHARED runtime-free native-expr compiler (native-expr.ts — the same
|
|
2053
|
-
* traversal, type-inference and fail-closed discipline rust uses, so the two agree by construction):
|
|
2054
|
-
*
|
|
2055
|
-
* - `opt($.x)` / `$.x` at port position → `*T` (the leaf receives the absent value as null, exactly
|
|
2056
|
-
* as run_behavior's `evalPorts` passes it).
|
|
2057
|
-
* - `coalesce(opt($.x), <literal>)` → the native deref-or-default — a native `T`, no boxed Value.
|
|
2058
|
-
* - `ne(opt($.x), null)` / `eq(…, null)` → the native presence test (`!= nil` / `== nil`).
|
|
2059
|
-
*
|
|
2060
|
-
* Returns null when the port expression reads NO optional input (the caller keeps its existing lowering).
|
|
2061
|
-
* A port that DOES read one but does not compile — or that compiles to a fallible expression, which this
|
|
2062
|
-
* position cannot hoist — FAILS CLOSED loudly rather than degrade to a required type or a zero value.
|
|
2063
|
-
*/
|
|
2064
|
-
function optPortCompileG(node, inputPorts, where, plan) {
|
|
2065
|
-
if (plan === undefined)
|
|
2066
|
-
return null;
|
|
2067
|
-
if (!exprReadsOptionalInput(node, inputPorts))
|
|
2068
|
-
return null;
|
|
2069
|
-
const resolveHead = (head) => goInputHeadRef(head, { inputPorts }, plan);
|
|
2070
|
-
let c;
|
|
2071
|
-
try {
|
|
2072
|
-
// zeroOut is unreachable here: a port expression that needs an error return is rejected below
|
|
2073
|
-
// (stmts.length > 0), so the backend never renders an early-return with it.
|
|
2074
|
-
c = new NativeExprCompiler(makeGoExprBackend("", resolveHead, plan)).compilePort(node);
|
|
2075
|
-
}
|
|
2076
|
-
catch (e) {
|
|
2077
|
-
if (!(e instanceof GeneratorFailure))
|
|
2078
|
-
throw e;
|
|
2079
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} ${JSON.stringify(node)} reads an OPTIONAL input port but does not lower to a native Go value: ${e.message}`);
|
|
2080
|
-
}
|
|
2081
|
-
if (c.stmts.length > 0)
|
|
2082
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} ${JSON.stringify(node)} reads an OPTIONAL input port through 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.`);
|
|
2083
|
-
return c;
|
|
2084
|
-
}
|
|
2085
|
-
/** exprReadsOptionalInput — does this port expression reference an OPTIONAL (`required:false`) input
|
|
2086
|
-
* port? Language-neutral scan for `ref`/`refOpt` heads bound to an optional port. Decides ONLY which
|
|
2087
|
-
* lane a port takes; the lane itself then proves the lowering (or fails closed). */
|
|
2088
|
-
function exprReadsOptionalInput(node, inputPorts) {
|
|
2089
|
-
if (node === null || typeof node !== "object")
|
|
2090
|
-
return false;
|
|
2091
|
-
if (Array.isArray(node))
|
|
2092
|
-
return node.some((el) => exprReadsOptionalInput(el, inputPorts));
|
|
2093
|
-
const rec = node;
|
|
2094
|
-
const keys = Object.keys(rec);
|
|
2095
|
-
if (keys.length === 1 && (keys[0] === "ref" || keys[0] === "refOpt")) {
|
|
2096
|
-
const path = rec[keys[0]];
|
|
2097
|
-
return Array.isArray(path) && typeof path[0] === "string" && inputPortIsOptional(inputPorts?.[path[0]]);
|
|
2098
|
-
}
|
|
2099
|
-
return keys.some((k) => exprReadsOptionalInput(rec[k], inputPorts));
|
|
2100
|
-
}
|
|
2101
1626
|
/**
|
|
2102
1627
|
* emitCondArm — the struct-native exec of a covered cond node (#108). Mirrors run_behavior's cond
|
|
2103
1628
|
* lower (behavior.ts:425-429): compile `if` to a native bool, materialize the TAKEN branch to the
|
|
@@ -2110,11 +1635,7 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut)
|
|
|
2110
1635
|
const c = node.cond;
|
|
2111
1636
|
const s = sanitize(node.id);
|
|
2112
1637
|
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
2113
|
-
const resolveHead = (
|
|
2114
|
-
if (priorHere(head))
|
|
2115
|
-
return { expr: typedLocal(head), ref: typedNodes.get(head) };
|
|
2116
|
-
return goInputHeadRef(head, comp, plan);
|
|
2117
|
-
};
|
|
1638
|
+
const resolveHead = goNativeResolveHead(comp, plan, typedNodes, priorHere);
|
|
2118
1639
|
const be = makeGoExprBackend(zeroOut, resolveHead, plan);
|
|
2119
1640
|
const compiler = new NativeExprCompiler(be);
|
|
2120
1641
|
const cond = compiler.compileBool(c.if);
|
|
@@ -2337,6 +1858,9 @@ function makeGoExprBackend(zeroOut, resolveHead, plan) {
|
|
|
2337
1858
|
notOp: (a) => `(!${a})`,
|
|
2338
1859
|
structLit: (name, inits) => `${name}{${inits.map((i) => `${goFieldName(i.field)}: ${i.expr}`).join(", ")}}`,
|
|
2339
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" }),
|
|
2340
1864
|
// an opt (`*T`) NONE literal is `nil` (the inner type is not needed for the untyped nil in Go).
|
|
2341
1865
|
optNone: (_innerTy) => "nil",
|
|
2342
1866
|
// opt (`*T`) null-presence test: present = `(expr != nil)`, absent = `(expr == nil)`.
|
|
@@ -2539,12 +2063,14 @@ var ExpectedSpecVersions = map[string]int64{"behavior": ${sv.behavior}, "express
|
|
|
2539
2063
|
}
|
|
2540
2064
|
// ── module assembly ──────────────────────────────────────────────────────────────────
|
|
2541
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);
|
|
2542
2069
|
// Fail closed if a covered read SHAPE lacks the typed annotations (no silent re-box).
|
|
2543
2070
|
for (const c of ctx.ir.components)
|
|
2544
|
-
assertTypedCoverage(c);
|
|
2545
|
-
const native = ctx.ir.components.filter(isNativeRaw);
|
|
2546
|
-
const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
|
|
2547
|
-
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));
|
|
2548
2074
|
const decls = emitTypeDecls(plan);
|
|
2549
2075
|
// #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler when it emits
|
|
2550
2076
|
// a fallible checked-arith helper call — determines whether the helper library + `math` import ship).
|
|
@@ -2559,11 +2085,7 @@ function emit(ctx) {
|
|
|
2559
2085
|
const runnerCodes = [];
|
|
2560
2086
|
for (const c of native) {
|
|
2561
2087
|
inStructs.push(emitInStruct(c, plan));
|
|
2562
|
-
const typedNodes =
|
|
2563
|
-
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
2564
|
-
for (const n of c.body)
|
|
2565
|
-
if (!isControlGate(n))
|
|
2566
|
-
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);
|
|
2567
2089
|
for (const n of c.body) {
|
|
2568
2090
|
if ("fanout" in n) {
|
|
2569
2091
|
// v3: a fanout node emits a per-id element ports struct + a batch struct (like a batched map).
|
|
@@ -2579,7 +2101,7 @@ function emit(ctx) {
|
|
|
2579
2101
|
}
|
|
2580
2102
|
else {
|
|
2581
2103
|
// #129: pass typedNodes so a leaf array port fed by a prior node's arr outType types as []ElemT.
|
|
2582
|
-
structs.push(emitPortsStruct(c, n,
|
|
2104
|
+
structs.push(emitPortsStruct(c, n, plan, typedNodes));
|
|
2583
2105
|
}
|
|
2584
2106
|
}
|
|
2585
2107
|
// CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
|
|
@@ -2608,7 +2130,7 @@ function emit(ctx) {
|
|
|
2608
2130
|
// minimal, runtime-free header.
|
|
2609
2131
|
if (nonNative.length > 0) {
|
|
2610
2132
|
const rejects = nonNative
|
|
2611
|
-
.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"}`)
|
|
2612
2134
|
.join("\n");
|
|
2613
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` +
|
|
2614
2136
|
`The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
|
|
@@ -2666,8 +2188,8 @@ function emit(ctx) {
|
|
|
2666
2188
|
*/
|
|
2667
2189
|
export function goTypedNativeObserve(ir, runtimeImport) {
|
|
2668
2190
|
const components = ir.components;
|
|
2669
|
-
const native = components.filter(isNativeRaw);
|
|
2670
2191
|
const plan = buildTypePlan(ir);
|
|
2192
|
+
const native = components.filter((c) => isNativeRaw(c, plan));
|
|
2671
2193
|
const serializers = emitSerializers(plan);
|
|
2672
2194
|
const marshallers = goTypedInternals.emitMarshallers(plan);
|
|
2673
2195
|
const mustHelpers = emitMustHelpers(plan);
|
|
@@ -2802,8 +2324,13 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
|
2802
2324
|
// compiles clean / unused-param-free).
|
|
2803
2325
|
const echoable = echoablePorts(c, node, ref, plan, typedNodes);
|
|
2804
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)}}`;
|
|
2805
2332
|
const echoBranch = echoable
|
|
2806
|
-
.map((ep) => `\tif src.echo == ${JSON.stringify(ep)} {\n\t\treturn ${
|
|
2333
|
+
.map((ep) => `\tif src.echo == ${JSON.stringify(ep)} {\n\t\treturn ${echoRow(ep)}, true\n\t}\n`)
|
|
2807
2334
|
.join("");
|
|
2808
2335
|
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, node.id)}(${portsParam} ${portsStructName(c.name, node.id)}, _ ${nodeBoundT}) (${rowT}, bool) {
|
|
2809
2336
|
src, ok := s.next(${JSON.stringify(node.component)})
|
|
@@ -2953,14 +2480,12 @@ ${arms}
|
|
|
2953
2480
|
`;
|
|
2954
2481
|
}
|
|
2955
2482
|
/** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
|
|
2956
|
-
* declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row
|
|
2957
|
-
*
|
|
2958
|
-
*
|
|
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). */
|
|
2959
2486
|
function echoablePorts(comp, node, ref, plan, typedNodes) {
|
|
2960
|
-
if (ref.kind === "named")
|
|
2961
|
-
return [];
|
|
2962
2487
|
const want = renderTypeRef(ref);
|
|
2963
|
-
return Object.keys(node.ports).filter((p) => portFieldGoType(node.ports[p], comp
|
|
2488
|
+
return Object.keys(node.ports).filter((p) => portFieldGoType(node.ports[p], comp, plan, typedNodes, `port '${p}'`) === want);
|
|
2964
2489
|
}
|
|
2965
2490
|
/** emitDecodeRow — emit (once, deduped) a decoder `decodeRow_<comp>_<node><suffix>(v Value) <rowT>`
|
|
2966
2491
|
* that materializes the scripted ok Value into the concrete row struct (typed fields = the outType /
|