behavior-contracts 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -29
- package/dist/generator/core.d.ts +14 -0
- package/dist/generator/core.d.ts.map +1 -1
- package/dist/generator/core.js +1 -0
- package/dist/generator/core.js.map +1 -1
- package/dist/generator/emit-raw-abi-go.d.ts.map +1 -1
- package/dist/generator/emit-raw-abi-go.js +2 -1
- package/dist/generator/emit-raw-abi-go.js.map +1 -1
- package/dist/generator/emit-straightline-go.d.ts +2 -0
- package/dist/generator/emit-straightline-go.d.ts.map +1 -1
- package/dist/generator/emit-straightline-go.js +37 -19
- package/dist/generator/emit-straightline-go.js.map +1 -1
- package/dist/generator/emit-straightline-native-go.d.ts +54 -0
- package/dist/generator/emit-straightline-native-go.d.ts.map +1 -0
- package/dist/generator/emit-straightline-native-go.js +835 -0
- package/dist/generator/emit-straightline-native-go.js.map +1 -0
- package/dist/generator/emit-straightline-native-rust.d.ts +27 -0
- package/dist/generator/emit-straightline-native-rust.d.ts.map +1 -0
- package/dist/generator/emit-straightline-native-rust.js +624 -0
- package/dist/generator/emit-straightline-native-rust.js.map +1 -0
- package/dist/generator/emit-straightline-rust.d.ts.map +1 -1
- package/dist/generator/emit-straightline-rust.js +5 -12
- package/dist/generator/emit-straightline-rust.js.map +1 -1
- package/dist/generator/emit-straightline-typed-go.d.ts +17 -0
- package/dist/generator/emit-straightline-typed-go.d.ts.map +1 -1
- package/dist/generator/emit-straightline-typed-go.js +4 -1
- package/dist/generator/emit-straightline-typed-go.js.map +1 -1
- package/dist/generator/emit-straightline-typed-rust.d.ts +16 -0
- package/dist/generator/emit-straightline-typed-rust.d.ts.map +1 -1
- package/dist/generator/emit-straightline-typed-rust.js +2 -0
- package/dist/generator/emit-straightline-typed-rust.js.map +1 -1
- package/dist/generator/emit-straightline-typescript.d.ts.map +1 -1
- package/dist/generator/emit-straightline-typescript.js +4 -10
- package/dist/generator/emit-straightline-typescript.js.map +1 -1
- package/dist/generator/emit-typed-native-go.d.ts +63 -0
- package/dist/generator/emit-typed-native-go.d.ts.map +1 -0
- package/dist/generator/emit-typed-native-go.js +1840 -0
- package/dist/generator/emit-typed-native-go.js.map +1 -0
- package/dist/generator/emit-typed-native-rust.d.ts +52 -0
- package/dist/generator/emit-typed-native-rust.d.ts.map +1 -0
- package/dist/generator/emit-typed-native-rust.js +1673 -0
- package/dist/generator/emit-typed-native-rust.js.map +1 -0
- package/dist/generator/index.d.ts +2 -0
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +16 -0
- package/dist/generator/index.js.map +1 -1
- package/dist/generator/straightline.d.ts +46 -0
- package/dist/generator/straightline.d.ts.map +1 -1
- package/dist/generator/straightline.js +65 -0
- package/dist/generator/straightline.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1840 @@
|
|
|
1
|
+
import { GeneratorFailure } from "./core.js";
|
|
2
|
+
import { goStraightlineInternals } from "./emit-straightline-go.js";
|
|
3
|
+
import { goTypedInternals } from "./emit-straightline-typed-go.js";
|
|
4
|
+
import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
|
|
5
|
+
import { concurrencyPlan, execPlan, analyzeSequentialOps, classifyExpr, buildComponentPlan, } from "./straightline.js";
|
|
6
|
+
const PKG = "dslcontracts";
|
|
7
|
+
/** goErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
|
|
8
|
+
* plane (runtime-free). The covered module declares `type BehaviorError struct { Code, Message string }`
|
|
9
|
+
* (see emitBehaviorErrorType); a `*BehaviorError` satisfies the runner's `error` return. This REPLACES
|
|
10
|
+
* the old `dslcontracts.NewBehaviorFailure`/`NewPlanFailure` (the last error-plane runtime coupling) —
|
|
11
|
+
* the SAME codes are preserved verbatim so the observed failure is byte-equal to run_behavior. */
|
|
12
|
+
function goErr(code, msgExpr) {
|
|
13
|
+
return `&BehaviorError{Code: ${JSON.stringify(code)}, Message: ${msgExpr}}`;
|
|
14
|
+
}
|
|
15
|
+
/** emitBehaviorErrorType — the LOCAL concrete error type baked into the covered module so a runner can
|
|
16
|
+
* fail closed with ZERO bc-runtime import. `*BehaviorError` satisfies `error`. */
|
|
17
|
+
function emitBehaviorErrorType() {
|
|
18
|
+
return `// BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
|
|
19
|
+
// covered runner returns *BehaviorError (which satisfies error) instead of a bc-runtime failure, so
|
|
20
|
+
// the fully-covered module imports ZERO bc runtime. Codes match run_behavior verbatim (byte-equal).
|
|
21
|
+
type BehaviorError struct {
|
|
22
|
+
Code string
|
|
23
|
+
Message string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func (e *BehaviorError) Error() string { return e.Code + ": " + e.Message }
|
|
27
|
+
|
|
28
|
+
// FailureCode exposes the stable failure code (byte-equal to run_behavior) WITHOUT a bc-runtime type:
|
|
29
|
+
// a caller that wants the code type-asserts the small local interface { FailureCode() string } rather
|
|
30
|
+
// than a bc failure type — keeping this module runtime-free while a harness can still read the code.
|
|
31
|
+
func (e *BehaviorError) FailureCode() string { return e.Code }`;
|
|
32
|
+
}
|
|
33
|
+
const { sanitize, emitExpr } = goStraightlineInternals;
|
|
34
|
+
const { goFieldName, renderTypeRef, goScalar, serializeTyped, emitTypeDecls, emitSerializers, emitTypedValue, emitMustHelpers, } = goTypedInternals;
|
|
35
|
+
/** typed node-result local — MUST match goTypedInternals.typedLocal (`t_<id>`) so the reused
|
|
36
|
+
* emitTypedValue (output assembly via struct field access) reads the same locals. */
|
|
37
|
+
function typedLocal(nodeId) {
|
|
38
|
+
return `t_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
39
|
+
}
|
|
40
|
+
function goPortFieldName(wire) {
|
|
41
|
+
const safe = wire.replace(/[^A-Za-z0-9_]/g, "_");
|
|
42
|
+
const head = safe.charAt(0);
|
|
43
|
+
return /[a-z]/.test(head) ? head.toUpperCase() + safe.slice(1) : "F_" + safe;
|
|
44
|
+
}
|
|
45
|
+
function portsStructName(compName, nodeId) {
|
|
46
|
+
// EXPORTED so a consumer (and the test bench, both in a DIFFERENT Go package) can name the ports
|
|
47
|
+
// type to implement the per-component Handler_<comp> interface's concrete Node_* method signatures.
|
|
48
|
+
return `PortsNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
49
|
+
}
|
|
50
|
+
function inferPortType(node, inputPorts) {
|
|
51
|
+
if (staticArrElems(node) !== null)
|
|
52
|
+
return "Value";
|
|
53
|
+
const e = classifyExpr(node);
|
|
54
|
+
switch (e.kind) {
|
|
55
|
+
case "str":
|
|
56
|
+
case "concat":
|
|
57
|
+
return "string";
|
|
58
|
+
case "bool":
|
|
59
|
+
return "bool";
|
|
60
|
+
case "ref": {
|
|
61
|
+
if (e.path.length === 1) {
|
|
62
|
+
const s = inputPorts[e.path[0]];
|
|
63
|
+
if (s) {
|
|
64
|
+
if (s.type === "string")
|
|
65
|
+
return "string";
|
|
66
|
+
if (s.type === "int")
|
|
67
|
+
return "int64";
|
|
68
|
+
if (s.type === "float")
|
|
69
|
+
return "float64";
|
|
70
|
+
if (s.type === "bool")
|
|
71
|
+
return "bool";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return "Value";
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
return "Value";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** A static string-array port (every element a static string literal) — the graphddb `projection`
|
|
81
|
+
* shape. Returns the list of literal strings, or null when the node is not that shape. */
|
|
82
|
+
function staticStringArrElems(node) {
|
|
83
|
+
const arr = staticArrElems(node);
|
|
84
|
+
if (arr === null)
|
|
85
|
+
return null;
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const el of arr) {
|
|
88
|
+
const e = classifyExpr(el);
|
|
89
|
+
if (e.kind !== "str")
|
|
90
|
+
return null;
|
|
91
|
+
out.push(e.value);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* portFieldGoType — the NATIVE Go type for a covered port field (bc#90 / runtime-free). The covered
|
|
97
|
+
* ports struct carries NO `dslcontracts.Value` field: every port lowers to a concrete Go type.
|
|
98
|
+
* - a static string-array (projection) → `[]string` (change #2);
|
|
99
|
+
* - a bare int/float number literal (e.g. Query `limit: 20`) → `int64`/`float64`;
|
|
100
|
+
* - a string/bool/scalar-input port → the scalar Go type (inferPortType).
|
|
101
|
+
* A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
|
|
102
|
+
* Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no
|
|
103
|
+
* `dslcontracts.Value` fallback on the native module). `where` names the emission site for the error.
|
|
104
|
+
*/
|
|
105
|
+
function portFieldGoType(node, inputPorts, where = "port") {
|
|
106
|
+
if (staticStringArrElems(node) !== null)
|
|
107
|
+
return "[]string";
|
|
108
|
+
const num = numLiteralGoExpr(node);
|
|
109
|
+
if (num !== null)
|
|
110
|
+
return Number.isInteger(node) ? "int64" : "float64";
|
|
111
|
+
const ft = inferPortType(node, inputPorts);
|
|
112
|
+
if (ft === "Value") {
|
|
113
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): ${where} '${JSON.stringify(node)}' does not lower to a native Go type (would need a boxed dslcontracts.Value). The covered read plane is runtime-free and admits NO boxed escape — the covered corpus ports are string / bool / static-string-array (projection) / bare-number (limit) only. Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
|
|
114
|
+
}
|
|
115
|
+
return ft;
|
|
116
|
+
}
|
|
117
|
+
function staticArrElems(node) {
|
|
118
|
+
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
119
|
+
return null;
|
|
120
|
+
const keys = Object.keys(node);
|
|
121
|
+
if (keys.length !== 1 || keys[0] !== "arr")
|
|
122
|
+
return null;
|
|
123
|
+
const arg = node.arr;
|
|
124
|
+
return Array.isArray(arg) ? arg : null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* isNativeRawComponent — a component covered by the combined read emitter. Strictly sequential
|
|
128
|
+
* (no real concurrency), body is ALL componentRef (no map/cond), default 'fail'/'continue'
|
|
129
|
+
* policy, relationKind is single/connection or absent, and EVERY node carries outType + the
|
|
130
|
+
* component carries outputType. relationKind:single / bindField ARE the point — inline exec makes
|
|
131
|
+
* the child-present decision from the real parent result. Ports AND the output are evaluated via
|
|
132
|
+
* the primitive SSoT over a Value-space portScope (so a dynamic operator in a port or an
|
|
133
|
+
* obj-assembly output is fine — the SSoT handles fail-closed semantics; only the RESULT plane +
|
|
134
|
+
* PORT container go native). A covered SHAPE that lacks outType is a fail-closed error raised at
|
|
135
|
+
* emit (see assertTypedCoverage) — never a silent re-box.
|
|
136
|
+
*/
|
|
137
|
+
/**
|
|
138
|
+
* numLiteralGoExpr — a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
|
|
139
|
+
* `classifyExpr` deliberately leaves a bare number `dynamic` (its checked int/float range rules are
|
|
140
|
+
* the evaluate SSoT, expr.go evalNumberLiteral), so a plain number never reaches the str/bool/null
|
|
141
|
+
* static branch. But a number literal IS statically resolvable to a native Go Value: the SAME
|
|
142
|
+
* range check the runtime applies is performed HERE at generation time (integral literal in the
|
|
143
|
+
* safe 2^53-1 window → int64; a fractional/exponent literal → the checked float64). Returns the Go
|
|
144
|
+
* expression for the literal's Value, or null when the node is not a bare number OR the integral
|
|
145
|
+
* literal is out of the safe window (that out-of-range case keeps the runtime INVALID_LITERAL
|
|
146
|
+
* semantics — the port is NOT covered natively, so the whole component falls through, never a
|
|
147
|
+
* silently-wrong inline). Mirrors coderuntime.evalNumberLiteral exactly.
|
|
148
|
+
*/
|
|
149
|
+
const NUM_SAFE_INT = 9007199254740991; // 2^53 - 1 (expr.go safeInt)
|
|
150
|
+
function numLiteralGoExpr(node) {
|
|
151
|
+
if (typeof node !== "number" || !Number.isFinite(node))
|
|
152
|
+
return null;
|
|
153
|
+
if (Number.isInteger(node)) {
|
|
154
|
+
if (node < -NUM_SAFE_INT || node > NUM_SAFE_INT)
|
|
155
|
+
return null; // out of safe window → not covered (runtime INVALID_LITERAL)
|
|
156
|
+
return `${PKG}.Value(int64(${node}))`;
|
|
157
|
+
}
|
|
158
|
+
// fractional / exponent literal → checked float64 (finite, already guaranteed above).
|
|
159
|
+
return `${PKG}.Value(float64(${node}))`;
|
|
160
|
+
}
|
|
161
|
+
/** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
|
|
162
|
+
* resolvable to a native Go Value with no NewObj / no evaluate interpreter. */
|
|
163
|
+
function portIsStatic(node) {
|
|
164
|
+
const arr = staticArrElems(node);
|
|
165
|
+
if (arr !== null)
|
|
166
|
+
return arr.every(portIsStatic);
|
|
167
|
+
if (numLiteralGoExpr(node) !== null)
|
|
168
|
+
return true; // bare number literal (e.g. Query `limit`)
|
|
169
|
+
const e = classifyExpr(node);
|
|
170
|
+
switch (e.kind) {
|
|
171
|
+
case "str":
|
|
172
|
+
case "bool":
|
|
173
|
+
case "null":
|
|
174
|
+
case "ref":
|
|
175
|
+
return true;
|
|
176
|
+
case "concat":
|
|
177
|
+
return e.parts.every((p) => portIsStatic(reconstructG(p)));
|
|
178
|
+
default:
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** The output must lower to a typed struct/value with no Value-scope read: a ref whose head is a
|
|
183
|
+
* BODY NODE (typed), an obj/arr of native-lowerable outputs, or a concat of them. A ref to an
|
|
184
|
+
* input param or a non-concat operator is NOT native-lowerable here (falls through). */
|
|
185
|
+
function outputIsNativeLowerable(comp) {
|
|
186
|
+
const bodyIds = new Set(comp.body.map((n) => n.id));
|
|
187
|
+
const walk = (node) => {
|
|
188
|
+
const arr = staticArrElems(node);
|
|
189
|
+
if (arr !== null)
|
|
190
|
+
return arr.every(walk);
|
|
191
|
+
if (node !== null && typeof node === "object" && !Array.isArray(node)) {
|
|
192
|
+
const keys = Object.keys(node);
|
|
193
|
+
if (keys.length === 1 && keys[0] === "obj") {
|
|
194
|
+
const obj = node.obj;
|
|
195
|
+
return Object.keys(obj).every((k) => walk(obj[k]));
|
|
196
|
+
}
|
|
197
|
+
if (keys.length === 1 && keys[0] === "arr")
|
|
198
|
+
return true; // handled by staticArrElems above
|
|
199
|
+
}
|
|
200
|
+
const e = classifyExpr(node);
|
|
201
|
+
if (e.kind === "str" || e.kind === "bool" || e.kind === "null")
|
|
202
|
+
return true;
|
|
203
|
+
// ref OR refOpt to a body node (typed). refOpt is how a produced-aware output reads a skippable
|
|
204
|
+
// relationSingle child: the head node's produced flag gates it, yielding opt-nil when unproduced.
|
|
205
|
+
if (e.kind === "ref")
|
|
206
|
+
return bodyIds.has(e.path[0]); // must ref a body node (typed), not input
|
|
207
|
+
if (e.kind === "concat")
|
|
208
|
+
return e.parts.every((p) => walk(reconstructG(p)));
|
|
209
|
+
return false;
|
|
210
|
+
};
|
|
211
|
+
return walk(comp.output);
|
|
212
|
+
}
|
|
213
|
+
/** A native-raw component MUST be fully typed (outType on every node + outputType). */
|
|
214
|
+
function isFullyTyped(comp) {
|
|
215
|
+
if (comp.outputType === undefined)
|
|
216
|
+
return false;
|
|
217
|
+
for (const n of comp.body) {
|
|
218
|
+
if (n.outType === undefined)
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
function isNativeRaw(comp) {
|
|
224
|
+
return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet). A BATCHED `map` node whose:
|
|
228
|
+
* - over is a static ref (to a prior node's typed collection field OR an input array),
|
|
229
|
+
* - `into` is set (zip-attach the batched handler result onto every over element),
|
|
230
|
+
* - every element port is static (literal / ref into the element binding / concat / static arr),
|
|
231
|
+
* - element outType is a NAMED struct (the augmented element type — over element + the into field),
|
|
232
|
+
* - no `when` guard (guard+into => heterogeneous elements, not one []T — stays boxed),
|
|
233
|
+
* - default 'fail' policy, relationKind single/connection or absent.
|
|
234
|
+
* This is the struct-native de-box of a typed `map...into` element collection: build the per-element
|
|
235
|
+
* native ports struct, dispatch the batched handler ONCE (RawValue out), and assemble each augmented
|
|
236
|
+
* element struct by copying the over element's typed fields + materializing the into field from the
|
|
237
|
+
* aligned batch raw — NO boxed Value on any plane.
|
|
238
|
+
*/
|
|
239
|
+
function coveredMapNode(n, bodyIds) {
|
|
240
|
+
if (n === null || typeof n !== "object" || !("map" in n))
|
|
241
|
+
return false;
|
|
242
|
+
const m = n.map;
|
|
243
|
+
// #86 pt2: batched map...into. #93 shape 2: NON-batched map...into (per-element dispatch). #93
|
|
244
|
+
// shape 3: batched map with NO into (connection-collect / plain list). A `when` guard + into is
|
|
245
|
+
// still uncovered (heterogeneous elements — not one []T). A `when` guard WITHOUT into would be
|
|
246
|
+
// covered in principle but the covered corpus does not exercise it; keep it out to stay conservative.
|
|
247
|
+
if (m.when !== undefined)
|
|
248
|
+
return false;
|
|
249
|
+
if (m.policy !== undefined && m.policy !== "fail")
|
|
250
|
+
return false;
|
|
251
|
+
if (m.relationKind !== undefined && m.relationKind !== "single" && m.relationKind !== "connection")
|
|
252
|
+
return false;
|
|
253
|
+
// `over` must be a ref to a PRIOR BODY NODE's typed arr field (the de-box resolves the over element
|
|
254
|
+
// type from the parent node's typed struct — mapOverElemType). A map over an INPUT-param array is
|
|
255
|
+
// not covered here (its element type comes from inputPorts, not a typed node struct) — it falls
|
|
256
|
+
// through to the boxed typed / straight-line path.
|
|
257
|
+
const overE = classifyExpr(m.over);
|
|
258
|
+
if (overE.kind !== "ref" || !bodyIds.has(overE.path[0]))
|
|
259
|
+
return false;
|
|
260
|
+
const ports = m.ports;
|
|
261
|
+
for (const p of Object.keys(ports))
|
|
262
|
+
if (!portIsStatic(ports[p]))
|
|
263
|
+
return false;
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
/** A covered read SHAPE (typing-independent) — sequential componentRef reads + covered map...into,
|
|
267
|
+
* OR a real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point
|
|
268
|
+
* reads. A real-concurrency plan whose parallel members include a map / bindField-relation child is
|
|
269
|
+
* NOT covered here (falls through) — the static-parallel orchestration covers the componentRef fan-out
|
|
270
|
+
* (the GroupAccessRead shape); a parallel map/relation-child stage is a further shape (out of scope). */
|
|
271
|
+
function isSequentialComponentRefRead(comp) {
|
|
272
|
+
return coverageRejectReason(comp) === null;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89). Returns
|
|
276
|
+
* null when the component IS a covered native read; otherwise a SPECIFIC human-readable reason naming
|
|
277
|
+
* the exact disqualifier and the offending node id. The predicate logic is IDENTICAL to
|
|
278
|
+
* isSequentialComponentRefRead (which is defined as `coverageRejectReason(c) === null`) so the two can
|
|
279
|
+
* never disagree. This is the message the fail-closed dispatch surfaces per non-native component — the
|
|
280
|
+
* user's signal to add native coverage, fix a genuinely-dynamic spec, or use the --ir dynamic surface.
|
|
281
|
+
*/
|
|
282
|
+
function coverageRejectReason(comp) {
|
|
283
|
+
const ep = execPlan(comp);
|
|
284
|
+
if (ep === null)
|
|
285
|
+
return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
|
|
286
|
+
// A parallel-stage member must be a plain componentRef point read (no map, no bindField) so the
|
|
287
|
+
// static parallel orchestration is a clean fan-out — the bounded-concurrency error-precedence
|
|
288
|
+
// protocol is proven for that shape. A map / bindField child inside a parallel stage falls through.
|
|
289
|
+
for (const [i] of ep.parallelStageOf) {
|
|
290
|
+
const n = comp.body[i];
|
|
291
|
+
if (!("component" in n))
|
|
292
|
+
return `node '${n.id}' is a non-componentRef member of a parallel stage (not native-covered)`;
|
|
293
|
+
if (n.bindField !== undefined)
|
|
294
|
+
return `node '${n.id}' is a bindField relation child inside a parallel stage (not native-covered)`;
|
|
295
|
+
}
|
|
296
|
+
const bodyIds = new Set(comp.body.map((b) => b.id));
|
|
297
|
+
for (const n of comp.body) {
|
|
298
|
+
if ("cond" in n)
|
|
299
|
+
return `node '${n.id}' is a cond branch (not yet native-covered)`;
|
|
300
|
+
if ("map" in n) {
|
|
301
|
+
// #86 pt2 batched map...into / #93 shape 2 non-batched map...into / #93 shape 3 batched map NO
|
|
302
|
+
// into ARE covered (over a prior body node's typed arr); other map shapes fall through.
|
|
303
|
+
if (!coveredMapNode(n, bodyIds))
|
|
304
|
+
return `node '${n.id}' is a non-covered map shape (guarded/over-input)`;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (!("component" in n))
|
|
308
|
+
return `node '${n.id}' is not a componentRef, map, or cond node (not native-covered)`;
|
|
309
|
+
const cr = n;
|
|
310
|
+
// Only default 'fail' policy on covered reads: 'continue' (and any skip source, incl. bindField)
|
|
311
|
+
// makes a node UNPRODUCED at runtime, whose output ref must yield the unproduced rep — a
|
|
312
|
+
// produced-aware output lowering the struct-only path does not do yet (documented follow-up).
|
|
313
|
+
// Such components fall through to the boxed typed / straight-line path (correct, just not
|
|
314
|
+
// struct-de-plumbed). relationKind:single WITHOUT bindField (the #74/#323 core) IS covered.
|
|
315
|
+
if (cr.policy !== undefined && cr.policy !== "fail")
|
|
316
|
+
return `node '${n.id}' componentRef policy '${cr.policy}' (produced-aware output not yet native-covered)`;
|
|
317
|
+
// #86 part 1: a relationKind:single child WITH bindField (relationSingle) IS covered. When the
|
|
318
|
+
// bound parent field is null/absent the child is UNPRODUCED — the produced-aware output lowering
|
|
319
|
+
// (emitProducedAwareValue) then yields the unproduced rep (null / opt-nil) for a ref/refOpt into
|
|
320
|
+
// it, matching run_behavior's writeUnproduced. #93 shape 1: a bindField on a relationKind:connection
|
|
321
|
+
// child is ALSO covered — the connection's unproduced rep is the empty connection {items:[],cursor:null}
|
|
322
|
+
// (unproducedValue("connection")), which emitProducedAwareValue emits explicitly for a skipped
|
|
323
|
+
// connection node (a ref into it yields {items:[],cursor:null}, matching run_behavior).
|
|
324
|
+
if (cr.bindField !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
325
|
+
return `node '${n.id}' relationKind '${cr.relationKind ?? "point"}' not native-covered (bindField only supports single|connection)`;
|
|
326
|
+
if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
327
|
+
return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
|
|
328
|
+
for (const p of Object.keys(cr.ports))
|
|
329
|
+
if (portIsStatic(cr.ports[p]) === false)
|
|
330
|
+
return `node '${n.id}' port '${p}' is not statically resolvable`;
|
|
331
|
+
}
|
|
332
|
+
if (!outputIsNativeLowerable(comp))
|
|
333
|
+
return "output is not natively lowerable";
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
/** Fail closed if a covered read SHAPE lacks the typed annotations (no silent boxed re-box). */
|
|
337
|
+
function assertTypedCoverage(comp) {
|
|
338
|
+
if (!isSequentialComponentRefRead(comp))
|
|
339
|
+
return;
|
|
340
|
+
if (isFullyTyped(comp))
|
|
341
|
+
return;
|
|
342
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#77): component '${comp.name}' is a sequential componentRef read but lacks outType/outputType on every node — the combined emitter de-boxes the RESULT into the node's outType struct, so a covered read MUST be fully typed (bc#45). Add outType annotations, or regenerate on go-straightline-native (boxed result).`);
|
|
343
|
+
}
|
|
344
|
+
// ── native ports struct + PortReader ──────────────────────────────────────────────────
|
|
345
|
+
function emitPortsStruct(comp, node) {
|
|
346
|
+
const structName = portsStructName(comp.name, node.id);
|
|
347
|
+
const portNames = Object.keys(node.ports);
|
|
348
|
+
if (portNames.length === 0) {
|
|
349
|
+
return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
|
|
350
|
+
}
|
|
351
|
+
const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp.inputPorts));
|
|
352
|
+
const fieldNames = portNames.map(goPortFieldName);
|
|
353
|
+
const fields = portNames
|
|
354
|
+
.map((p, i) => `\t${fieldNames[i]} ${fieldTypes[i]} // ${JSON.stringify(p)}`)
|
|
355
|
+
.join("\n");
|
|
356
|
+
// NO PortReader `Port(name)` method: the covered handler reads the TYPED struct fields (ports.F_PK)
|
|
357
|
+
// DIRECTLY, so the boxed-Value PortReader impl (the last per-port `dslcontracts.Value` wrap on the
|
|
358
|
+
// covered plane) is DEAD — dropping it makes the ports struct fully native (bc#90 / runtime-free).
|
|
359
|
+
return `// ${structName} — NATIVE ports for node '${node.id}' (${node.component}). Typed fields per the
|
|
360
|
+
// static port type; built by direct field assignment (no map alloc, no per-port boxing). The consumer
|
|
361
|
+
// reads the typed fields DIRECTLY (no PortReader / boxed-Value accessor) — the covered read is native.
|
|
362
|
+
type ${structName} struct {
|
|
363
|
+
${fields}
|
|
364
|
+
}`;
|
|
365
|
+
}
|
|
366
|
+
// ── CONCRETE per-node row structs + row→outType copiers (bc: full Go concreteness) ──────────
|
|
367
|
+
//
|
|
368
|
+
// The covered path no longer crosses a generic `RawValue`/`RawRow` on the RESULT plane. Each
|
|
369
|
+
// covered node has a CONCRETE row struct `RawRow_<comp>_<node>` whose fields are the node's
|
|
370
|
+
// projected outType fields (typed, flattened) plus a per-node error signal (IsError/Err). The
|
|
371
|
+
// per-component `Handler_<comp>` interface has one method per node returning that concrete struct;
|
|
372
|
+
// the runner reads `row.<Field>` DIRECTLY (no `.(RawRow)`, no `.(scalar)`, no `RawValue`) and copies
|
|
373
|
+
// each field into the node's outType struct local. The `.(T)` type-assert seam that the generic
|
|
374
|
+
// RawValue de-box carried is GONE on the covered path (the handler produces the typed fields).
|
|
375
|
+
//
|
|
376
|
+
// A scalar/arr/opt node (outType not a named struct) has a single `Val <typed>` payload field. A
|
|
377
|
+
// named-struct node has one Go field per outType field (same names/types as the outType decl), so
|
|
378
|
+
// `RawRow_<comp>_<node>` is structurally the outType struct + {IsError, Err}. The copier is then a
|
|
379
|
+
// mechanical field-for-field assignment into the outType struct — NO dynamic value read anywhere.
|
|
380
|
+
/** Row_<comp>_<node> — the concrete per-node handler-result row struct name. (Deliberately NOT
|
|
381
|
+
* prefixed with the generic dynamic-accessor name — that accessor is what the concrete path removes.) */
|
|
382
|
+
function rawRowStructName(compName, nodeId) {
|
|
383
|
+
return `Row_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
384
|
+
}
|
|
385
|
+
/** RowElem_<comp>_<node> — the concrete per-ELEMENT row struct name for a covered map node (the
|
|
386
|
+
* element/into row the map handler yields per over element). */
|
|
387
|
+
function rawElemStructName(compName, nodeId) {
|
|
388
|
+
return `RowElem_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
389
|
+
}
|
|
390
|
+
/** Handler_<comp> — the per-component concrete handler interface name (one Node_* method per node). */
|
|
391
|
+
function handlerIfaceName(compName) {
|
|
392
|
+
return `Handler_${sanitize(compName)}`;
|
|
393
|
+
}
|
|
394
|
+
/** Node_<comp>_<nodeId> — the concrete per-node handler method name. */
|
|
395
|
+
function nodeMethodName(compName, nodeId) {
|
|
396
|
+
return `Node_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* emitRawRowStructs — emit the concrete `RawRow_<comp>_<node>` struct per covered node (and, for a
|
|
400
|
+
* covered map node, the per-element `RawElem_<comp>_<node>`). A named-struct outType flattens to one
|
|
401
|
+
* Go field per outType field; a scalar/arr/opt node carries a single `Val` payload. Every row carries
|
|
402
|
+
* the per-node error signal (IsError/Err) so the runner interprets policy without a boxed outcome.
|
|
403
|
+
*/
|
|
404
|
+
function emitRawRowStructs(comp, typedNodes, plan) {
|
|
405
|
+
const parts = [];
|
|
406
|
+
for (const n of comp.body) {
|
|
407
|
+
const ref = typedNodes.get(n.id);
|
|
408
|
+
if ("map" in n) {
|
|
409
|
+
// a covered map node: RawElem_<comp>_<node> is the HANDLER'S per-element return shape:
|
|
410
|
+
// - into: the `into` field's type (the fetched item merged onto every over element);
|
|
411
|
+
// - no-into (#93 shape 3): the element outType directly (connection-collect).
|
|
412
|
+
// The batched Node_* returns RawRow_<comp>_<node> (aligned []RawElem_ + error); a per-element
|
|
413
|
+
// dispatch Node_* returns RawElem_ directly (one physical call per element).
|
|
414
|
+
const arrRef = ref;
|
|
415
|
+
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
416
|
+
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
417
|
+
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
|
|
418
|
+
`type ${rawRowStructName(comp.name, n.id)} struct {\n\tIsError bool\n\tErr string\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
|
|
422
|
+
}
|
|
423
|
+
return parts.join("\n\n");
|
|
424
|
+
}
|
|
425
|
+
/** Emit one concrete row struct: flatten a named outType to typed fields, else a single Val payload;
|
|
426
|
+
* always carry IsError/Err. */
|
|
427
|
+
function emitOneRowStruct(name, ref, plan) {
|
|
428
|
+
const lines = [];
|
|
429
|
+
lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType; + error signal).`);
|
|
430
|
+
lines.push(`type ${name} struct {`);
|
|
431
|
+
lines.push(`\tIsError bool`);
|
|
432
|
+
lines.push(`\tErr string`);
|
|
433
|
+
if (ref.kind === "named") {
|
|
434
|
+
const decl = findDecl(plan, ref.name);
|
|
435
|
+
for (const f of decl.fields) {
|
|
436
|
+
lines.push(`\t${goFieldName(f.name)} ${renderTypeRef(f.type)} // ${JSON.stringify(f.name)}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
lines.push(`\tVal ${renderTypeRef(ref)}`);
|
|
441
|
+
}
|
|
442
|
+
lines.push(`}`);
|
|
443
|
+
return lines.join("\n");
|
|
444
|
+
}
|
|
445
|
+
/** emitRowCopy — the statements copying a concrete row's typed fields into the node's outType local
|
|
446
|
+
* `target` (a value of Go type renderTypeRef(ref)). Named: field-for-field; scalar/arr/opt: `.Val`.
|
|
447
|
+
* `rowExpr` is the concrete RawRow_/RawElem_ value; `indent` tabs. No dynamic read — pure assignment. */
|
|
448
|
+
function emitRowCopy(target, rowExpr, ref, plan, indent) {
|
|
449
|
+
const t = "\t".repeat(indent);
|
|
450
|
+
if (ref.kind === "named") {
|
|
451
|
+
const decl = findDecl(plan, ref.name);
|
|
452
|
+
return decl.fields.map((f) => `${t}${target}.${goFieldName(f.name)} = ${rowExpr}.${goFieldName(f.name)}`).join("\n");
|
|
453
|
+
}
|
|
454
|
+
return `${t}${target} = ${rowExpr}.Val`;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* boundGoType (change #3 — typed `bound`) — the CONCRETE Go type of a covered node's `bound` handler
|
|
458
|
+
* param (bc#90 / runtime-free). `bound` is the parent-produced key (the bindField value); it replaces
|
|
459
|
+
* the old boxed `dslcontracts.Value`. We represent PRODUCED-vs-UNPRODUCED with a POINTER: a non-nil
|
|
460
|
+
* `*T` means the parent value was produced/present (an empty string is a VALID produced value — a
|
|
461
|
+
* non-nil `*string` to ""); a nil pointer means UNPRODUCED/skip (exactly the old `!= nil` guard ≡
|
|
462
|
+
* run_behavior). The pointed-to type is the bindField's concrete Go type (a string key here). A node
|
|
463
|
+
* WITHOUT a bindField never receives a bound value — its param is a `*string` placeholder always
|
|
464
|
+
* passed `nil` (a typed nil, so the ABI stays uniform and carries NO dslcontracts.Value).
|
|
465
|
+
*/
|
|
466
|
+
function boundGoType(node, comp, typedNodes, plan) {
|
|
467
|
+
if (node.bindField === undefined || node.parent === undefined)
|
|
468
|
+
return "*string";
|
|
469
|
+
const parentRef = typedNodes.get(node.parent);
|
|
470
|
+
if (parentRef === undefined)
|
|
471
|
+
return "*string";
|
|
472
|
+
let bfRef;
|
|
473
|
+
try {
|
|
474
|
+
bfRef = goTypedInternals.typedFieldAccess(typedLocal(node.parent), parentRef, [node.bindField], plan).ref;
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return "*string";
|
|
478
|
+
}
|
|
479
|
+
// opt bindField: the field is already a *T — the bound pointed-to type is the inner. A required
|
|
480
|
+
// scalar bindField: the bound pointed-to type is that scalar's Go type.
|
|
481
|
+
if (bfRef.kind === "opt")
|
|
482
|
+
return `*${renderTypeRef(bfRef.inner)}`;
|
|
483
|
+
return `*${renderTypeRef(bfRef)}`;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* emitHandlerInterface — the per-component `Handler_<comp>` interface: one concrete-typed `Node_*`
|
|
487
|
+
* method per covered node. Each componentRef node method takes the node's native ports struct + the
|
|
488
|
+
* typed `bound` pointer (change #3 — the produced-aware parent key; NO boxed dslcontracts.Value) and
|
|
489
|
+
* returns its concrete row struct + a resolved bool. A covered map node's method takes the batch
|
|
490
|
+
* ports struct (batched) or the element ports struct (per-element) and returns the batch / element
|
|
491
|
+
* row. This is the fully-concrete, runtime-free replacement for the generic ExecNativeRaw seam.
|
|
492
|
+
*/
|
|
493
|
+
function emitHandlerInterface(comp, typedNodes, plan) {
|
|
494
|
+
const lines = [];
|
|
495
|
+
lines.push(`// ${handlerIfaceName(comp.name)} — the CONCRETE per-component handler seam: one typed method`);
|
|
496
|
+
lines.push(`// per covered node (native ports struct IN, concrete row struct OUT). No generic boxed-ports`);
|
|
497
|
+
lines.push(`// / boxed-value / dynamic accessor crosses the covered boundary — the consumer implements each`);
|
|
498
|
+
lines.push(`// Node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
|
|
499
|
+
lines.push(`type ${handlerIfaceName(comp.name)} interface {`);
|
|
500
|
+
for (const n of comp.body) {
|
|
501
|
+
const method = nodeMethodName(comp.name, n.id);
|
|
502
|
+
const bt = boundGoType(n, comp, typedNodes, plan);
|
|
503
|
+
if ("map" in n) {
|
|
504
|
+
const m = n.map;
|
|
505
|
+
const batched = m.batched === true;
|
|
506
|
+
const portsT = batched ? `${portsStructName(comp.name, n.id)}_batch` : portsStructName(comp.name, n.id);
|
|
507
|
+
const retT = batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
|
|
508
|
+
lines.push(`\t${method}(ports ${portsT}, bound ${bt}) (${retT}, bool)`);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
lines.push(`\t${method}(ports ${portsStructName(comp.name, n.id)}, bound ${bt}) (${rawRowStructName(comp.name, n.id)}, bool)`);
|
|
512
|
+
}
|
|
513
|
+
lines.push(`}`);
|
|
514
|
+
return lines.join("\n");
|
|
515
|
+
}
|
|
516
|
+
/** mapElemMaterializerName — the augmented-element copier for a covered map...into node. */
|
|
517
|
+
function mapElemMaterializerName(compName, nodeId) {
|
|
518
|
+
return `materializeMapElemNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* emitMapPortsStructs — the per-element native ports struct + the batch struct for a covered map
|
|
522
|
+
* node (bc#90 / runtime-free). The element struct is the same shape as a componentRef ports struct
|
|
523
|
+
* (typed fields per the static port type). The batch struct carries `Items []<elemStruct>` — a NATIVE
|
|
524
|
+
* slice of the concrete per-element ports structs (NO `[]dslcontracts.Value`, NO PortReader accessor);
|
|
525
|
+
* the consumer reads `bp.Items[i].F_PK` directly.
|
|
526
|
+
*/
|
|
527
|
+
function emitMapPortsStructs(comp, node) {
|
|
528
|
+
const m = node.map;
|
|
529
|
+
const structName = portsStructName(comp.name, node.id);
|
|
530
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports });
|
|
531
|
+
const batchStruct = `${structName}_batch`;
|
|
532
|
+
return `${elemStruct}
|
|
533
|
+
|
|
534
|
+
// ${batchStruct} — NATIVE batched ports for map '${node.id}': Items is a native slice of the concrete
|
|
535
|
+
// per-element ports structs (${structName}). The consumer reads Items[i].<Field> DIRECTLY — no boxed
|
|
536
|
+
// []Value, no PortReader accessor, no key-value object (bc#90 / runtime-free).
|
|
537
|
+
type ${batchStruct} struct {
|
|
538
|
+
Items []${structName}
|
|
539
|
+
}`;
|
|
540
|
+
}
|
|
541
|
+
/** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
|
|
542
|
+
* type (into) or the element outType directly (no-into). This is the type of RawElem_<comp>_<node>. */
|
|
543
|
+
function mapElemRowRef(node, arrRef, plan) {
|
|
544
|
+
const m = node.map;
|
|
545
|
+
if (m.into === undefined)
|
|
546
|
+
return arrRef.elem;
|
|
547
|
+
if (arrRef.elem.kind !== "named") {
|
|
548
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined map (#86 pt2): node '${node.id}' element outType must be a named struct (got ${JSON.stringify(arrRef.elem.kind)}) — the augmented element de-boxes into a struct. Regenerate as boxed typed/straight-line.`);
|
|
549
|
+
}
|
|
550
|
+
const decl = findDecl(plan, arrRef.elem.name);
|
|
551
|
+
const intoField = decl.fields.find((f) => f.name === m.into);
|
|
552
|
+
if (!intoField) {
|
|
553
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined map (#86 pt2): element struct '${arrRef.elem.name}' has no '${m.into}' (into) field.`);
|
|
554
|
+
}
|
|
555
|
+
return intoField.type;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* emitMapElemMaterializers — for each covered map...into node, emit the AUGMENTED-element copier: copy
|
|
559
|
+
* the over element's typed fields + assign the `into` field from the concrete per-element handler row
|
|
560
|
+
* (RawElem_<comp>_<node>). Pure struct field assignment — NO dynamic value read (the handler already
|
|
561
|
+
* produced the typed into row).
|
|
562
|
+
*/
|
|
563
|
+
function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
564
|
+
const parts = [];
|
|
565
|
+
for (const n of comp.body) {
|
|
566
|
+
if (!("map" in n))
|
|
567
|
+
continue;
|
|
568
|
+
const m = n.map;
|
|
569
|
+
// #93 shape 3: a map with NO `into` collects each element DIRECTLY from the per-element handler row
|
|
570
|
+
// (RawElem_ IS the element outType) — no over-element merge, so no augmented-element copier here.
|
|
571
|
+
if (m.into === undefined)
|
|
572
|
+
continue;
|
|
573
|
+
const arrRef = typedNodes.get(n.id);
|
|
574
|
+
const elemRef = arrRef.elem;
|
|
575
|
+
const decl = findDecl(plan, elemRef.name);
|
|
576
|
+
const intoKey = m.into;
|
|
577
|
+
// over element type (all element fields EXCEPT the into field): these come from the over element
|
|
578
|
+
// struct (the parent collection element). We copy each by same-named field access.
|
|
579
|
+
const overFields = decl.fields.filter((f) => f.name !== intoKey);
|
|
580
|
+
const intoRowRef = mapElemRowRef(n, arrRef, plan);
|
|
581
|
+
const fn = mapElemMaterializerName(comp.name, n.id);
|
|
582
|
+
const overElemGo = mapOverElemType(comp, n, typedNodes, plan);
|
|
583
|
+
const elemRowGo = rawElemStructName(comp.name, n.id);
|
|
584
|
+
const lines = [];
|
|
585
|
+
lines.push(`// ${fn} — assemble the augmented map element '${n.id}': copy the over element's typed`);
|
|
586
|
+
lines.push(`// fields + set the '${intoKey}' (into) field from the concrete per-element handler row.`);
|
|
587
|
+
lines.push(`func ${fn}(over ${overElemGo}, into ${elemRowGo}) ${elemRef.name} {`);
|
|
588
|
+
lines.push(`\tvar out ${elemRef.name}`);
|
|
589
|
+
for (const f of overFields) {
|
|
590
|
+
lines.push(`\tout.${goFieldName(f.name)} = over.${goFieldName(f.name)}`);
|
|
591
|
+
}
|
|
592
|
+
lines.push(emitRowCopy(`out.${goFieldName(intoKey)}`, "into", intoRowRef, plan, 1));
|
|
593
|
+
lines.push(`\treturn out`);
|
|
594
|
+
lines.push(`}`);
|
|
595
|
+
parts.push(lines.join("\n"));
|
|
596
|
+
}
|
|
597
|
+
return parts.join("\n\n");
|
|
598
|
+
}
|
|
599
|
+
/** The Go type of the over-element (the parent collection element) for a covered map node. `over`
|
|
600
|
+
* is a static ref to a prior node's arr<named> field (or an input array). We resolve it via the
|
|
601
|
+
* parent node's typed struct field type. */
|
|
602
|
+
function mapOverElemType(comp, node, typedNodes, plan) {
|
|
603
|
+
const m = node.map;
|
|
604
|
+
const e = classifyExpr(m.over);
|
|
605
|
+
if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
|
|
606
|
+
const baseRef = typedNodes.get(e.path[0]);
|
|
607
|
+
const { ref } = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
|
|
608
|
+
if (ref.kind === "arr")
|
|
609
|
+
return renderTypeRef(ref.elem);
|
|
610
|
+
}
|
|
611
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined map (#86 pt2): map '${node.id}' 'over' must be a ref to a prior node's typed arr field (got ${JSON.stringify(m.over)}).`);
|
|
612
|
+
}
|
|
613
|
+
function classifyPort(node) {
|
|
614
|
+
if (staticArrElems(node) !== null)
|
|
615
|
+
return "static";
|
|
616
|
+
const e = classifyExpr(node);
|
|
617
|
+
switch (e.kind) {
|
|
618
|
+
case "str":
|
|
619
|
+
case "bool":
|
|
620
|
+
case "null":
|
|
621
|
+
case "ref":
|
|
622
|
+
return "static";
|
|
623
|
+
case "concat":
|
|
624
|
+
return e.parts.every((p) => classifyPort(reconstructG(p)) === "static") ? "static" : "dynamic";
|
|
625
|
+
default:
|
|
626
|
+
return "dynamic";
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function reconstructG(e) {
|
|
630
|
+
switch (e.kind) {
|
|
631
|
+
case "str":
|
|
632
|
+
return e.value;
|
|
633
|
+
case "bool":
|
|
634
|
+
return e.value;
|
|
635
|
+
case "null":
|
|
636
|
+
return null;
|
|
637
|
+
case "ref":
|
|
638
|
+
return { [e.opt ? "refOpt" : "ref"]: e.path };
|
|
639
|
+
case "concat":
|
|
640
|
+
return { concat: e.parts.map(reconstructG) };
|
|
641
|
+
case "dynamic":
|
|
642
|
+
return e.node;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// bc#90 / runtime-free: the old nvVE-based `emitStaticVE` / `valueLeaf` port lowering (which built
|
|
646
|
+
// `dslcontracts.Value` leaves via the nv* helpers) is REMOVED — every covered port now lowers to a
|
|
647
|
+
// FULLY NATIVE Go value in emitPortInit (string / bool / static-string-array / number literal), and a
|
|
648
|
+
// genuinely-dynamic port fails closed. There is no boxed-Value fallback on the covered read plane.
|
|
649
|
+
// ── typed input struct (In_<comp>) + NATIVE scalar port lowering ─────────────────────────────
|
|
650
|
+
//
|
|
651
|
+
// A covered runner takes a CONCRETE per-component input struct `In_<comp>` (fields = the component's
|
|
652
|
+
// inputPorts with their declared Go types) instead of a generic *Obj. A simple string/scalar port —
|
|
653
|
+
// a literal, a ref to a typed input field (`in.UserId`), a ref to a prior node's typed struct scalar
|
|
654
|
+
// field, or a concat of statically-string parts — then lowers DIRECTLY to a native Go expression
|
|
655
|
+
// (`"USER#" + in.UserId`) assigned straight into the concrete ports-struct field: NO dslcontracts.Value,
|
|
656
|
+
// NO nvVE/nvBindVE helper, NO `.(string)` type-assert. The value is statically the concrete type (the
|
|
657
|
+
// input schema / prior-node outType guarantee it, and literals are self-evidently typed), so dropping
|
|
658
|
+
// the runtime TYPE_MISMATCH check is semantics-preserving. A port that CANNOT be statically proven the
|
|
659
|
+
// concrete type (a ref to a non-matching-typed input, a refOpt / opt field, or a dynamic operator)
|
|
660
|
+
// falls back to the nvVE Value path (documented — the only place a `.(T)` could survive; the covered
|
|
661
|
+
// string key/table/projection ports never hit it).
|
|
662
|
+
/** In_<comp> — the concrete per-component input struct name (fields = inputPorts). */
|
|
663
|
+
function inStructName(compName) {
|
|
664
|
+
return `In_${sanitize(compName)}`;
|
|
665
|
+
}
|
|
666
|
+
/** Go type for an input port's declared type (scalar → concrete; anything else → Value). */
|
|
667
|
+
function inputPortGoType(schema) {
|
|
668
|
+
switch (schema?.type) {
|
|
669
|
+
case "string":
|
|
670
|
+
return "string";
|
|
671
|
+
case "int":
|
|
672
|
+
return "int64";
|
|
673
|
+
case "float":
|
|
674
|
+
return "float64";
|
|
675
|
+
case "bool":
|
|
676
|
+
return "bool";
|
|
677
|
+
default:
|
|
678
|
+
return `${PKG}.Value`;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/** The scalar kind an input port declares, or undefined if it is not a native scalar. */
|
|
682
|
+
function inputScalarKind(schema) {
|
|
683
|
+
switch (schema?.type) {
|
|
684
|
+
case "string":
|
|
685
|
+
return "string";
|
|
686
|
+
case "int":
|
|
687
|
+
return "int64";
|
|
688
|
+
case "float":
|
|
689
|
+
return "float64";
|
|
690
|
+
case "bool":
|
|
691
|
+
return "bool";
|
|
692
|
+
default:
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/** emitInStruct — the concrete input struct declaration for a covered component (fields = inputPorts,
|
|
697
|
+
* declaration order = Object key order; gofmt-aligned). Empty inputPorts => empty struct. */
|
|
698
|
+
function emitInStruct(comp) {
|
|
699
|
+
const name = inStructName(comp.name);
|
|
700
|
+
const ports = Object.keys(comp.inputPorts ?? {});
|
|
701
|
+
if (ports.length === 0) {
|
|
702
|
+
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
|
|
703
|
+
}
|
|
704
|
+
const names = ports.map((p) => goFieldName(p));
|
|
705
|
+
const types = ports.map((p) => inputPortGoType(comp.inputPorts[p]));
|
|
706
|
+
const nameW = Math.max(0, ...names.map((n) => n.length));
|
|
707
|
+
const typeW = Math.max(0, ...types.map((t) => t.length));
|
|
708
|
+
const fields = ports
|
|
709
|
+
.map((p, i) => `\t${names[i].padEnd(nameW)} ${types[i].padEnd(typeW)} // ${JSON.stringify(p)}`)
|
|
710
|
+
.join("\n");
|
|
711
|
+
return `// ${name} — the CONCRETE input for '${comp.name}' (fields = inputPorts; typed, consumer-built —
|
|
712
|
+
// NO generic *Obj, NO per-field boxing crosses the covered read boundary).
|
|
713
|
+
type ${name} struct {
|
|
714
|
+
${fields}
|
|
715
|
+
}`;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* emitInDecoder — the TEST-glue decoder `decode_In_<comp>(input *Obj) (In_<comp>, error)`. It reads
|
|
719
|
+
* each declared input port from the generic *Obj into the concrete In_<comp> field (scalar → typed
|
|
720
|
+
* read with a `.(T)` on the SDK/wire Value; Value-typed field → the raw Value). Lives ONLY in the
|
|
721
|
+
* observe companion (test harness), never in the covered module — the covered runner takes In_<comp>
|
|
722
|
+
* pre-built by the consumer. A missing key leaves the zero value (every covered input is required +
|
|
723
|
+
* always supplied by the corpus); a present-but-wrong-scalar is a fail-closed decode error.
|
|
724
|
+
*/
|
|
725
|
+
function emitInDecoder(comp) {
|
|
726
|
+
const name = inStructName(comp.name);
|
|
727
|
+
const ports = Object.keys(comp.inputPorts ?? {});
|
|
728
|
+
const lines = [];
|
|
729
|
+
lines.push(`func decode_${name}(input *${PKG}.Obj) (${name}, error) {`);
|
|
730
|
+
lines.push(`\tvar in ${name}`);
|
|
731
|
+
if (ports.length === 0 || comp.inputPorts === undefined) {
|
|
732
|
+
lines.push(`\t_ = input`);
|
|
733
|
+
lines.push(`\treturn in, nil`);
|
|
734
|
+
lines.push(`}`);
|
|
735
|
+
return lines.join("\n");
|
|
736
|
+
}
|
|
737
|
+
lines.push(`\tif input == nil {`);
|
|
738
|
+
lines.push(`\t\treturn in, nil`);
|
|
739
|
+
lines.push(`\t}`);
|
|
740
|
+
for (const p of ports) {
|
|
741
|
+
const field = goFieldName(p);
|
|
742
|
+
const scalar = inputScalarKind(comp.inputPorts[p]);
|
|
743
|
+
lines.push(`\tif v, ok := input.Get(${JSON.stringify(p)}); ok {`);
|
|
744
|
+
if (scalar === undefined) {
|
|
745
|
+
lines.push(`\t\tin.${field} = v`);
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
lines.push(`\t\tt, tok := v.(${scalar})`);
|
|
749
|
+
lines.push(`\t\tif !tok {`);
|
|
750
|
+
lines.push(`\t\t\treturn in, ${PKG}.NewExprFailure("TYPE_MISMATCH", "input ${p}: expected ${scalar}, got "+${PKG}.TypeName(v))`);
|
|
751
|
+
lines.push(`\t\t}`);
|
|
752
|
+
lines.push(`\t\tin.${field} = t`);
|
|
753
|
+
}
|
|
754
|
+
lines.push(`\t}`);
|
|
755
|
+
}
|
|
756
|
+
lines.push(`\treturn in, nil`);
|
|
757
|
+
lines.push(`}`);
|
|
758
|
+
return lines.join("\n");
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* emitNativeScalar — lower a static port expr to a NATIVE Go expression of the concrete `want` scalar
|
|
762
|
+
* type, or null if it cannot be statically proven that type. Literals: a string literal is native for
|
|
763
|
+
* `want==="string"`, a bool literal for `want==="bool"`. A ref resolves via `resolveRef` (typed input
|
|
764
|
+
* field / prior-node scalar / element binding). A concat lowers ONLY into `string` and only when EVERY
|
|
765
|
+
* part is itself a native string (so the concat's strings-only TYPE_MISMATCH can never fire — the parts
|
|
766
|
+
* are statically strings), emitting `p0 + p1 + …`. Returns null (fall back to the Value path) otherwise.
|
|
767
|
+
*/
|
|
768
|
+
function emitNativeScalar(node, want, resolveRef) {
|
|
769
|
+
// a static arr is a Value port (never a scalar) — not native-lowerable here.
|
|
770
|
+
if (staticArrElems(node) !== null)
|
|
771
|
+
return null;
|
|
772
|
+
const e = classifyExpr(node);
|
|
773
|
+
switch (e.kind) {
|
|
774
|
+
case "str":
|
|
775
|
+
return want === "string" ? JSON.stringify(e.value) : null;
|
|
776
|
+
case "bool":
|
|
777
|
+
return want === "bool" ? (e.value ? "true" : "false") : null;
|
|
778
|
+
case "null":
|
|
779
|
+
return null; // a null literal is a Value(nil), never a required native scalar
|
|
780
|
+
case "ref": {
|
|
781
|
+
const sc = resolveRef(e.path[0], e.path.slice(1), e.opt);
|
|
782
|
+
if (sc === null || sc.scalar !== want)
|
|
783
|
+
return null;
|
|
784
|
+
return sc.expr;
|
|
785
|
+
}
|
|
786
|
+
case "concat": {
|
|
787
|
+
if (want !== "string")
|
|
788
|
+
return null;
|
|
789
|
+
const parts = [];
|
|
790
|
+
for (const p of e.parts) {
|
|
791
|
+
const sub = emitNativeScalar(reconstructG(p), "string", resolveRef);
|
|
792
|
+
if (sub === null)
|
|
793
|
+
return null;
|
|
794
|
+
// parenthesize a `+` sub-expression so string concatenation binds correctly (a ref/literal is
|
|
795
|
+
// atomic; a nested concat is already a single `a + b` — wrap to keep it one operand).
|
|
796
|
+
parts.push(sub.includes(" + ") ? `(${sub})` : sub);
|
|
797
|
+
}
|
|
798
|
+
return parts.join(" + ");
|
|
799
|
+
}
|
|
800
|
+
default:
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* emitPortInit — emit the port field build for one port as a FULLY NATIVE Go expression (bc#90 /
|
|
806
|
+
* runtime-free). Every covered port lowers to a concrete Go value assigned straight into the ports
|
|
807
|
+
* struct field — NO `dslcontracts.Value`, NO nvVE helper, NO `.(T)` type-assert:
|
|
808
|
+
* - a static string array (projection) → `[]string{"a", "b", ...}` (change #2);
|
|
809
|
+
* - a bare int/float number literal (e.g. Query `limit: 20`) → `int64(20)` / `float64(...)`;
|
|
810
|
+
* - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
|
|
811
|
+
* A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered
|
|
812
|
+
* plane — a boxed escape would re-introduce the bc-runtime coupling this module removes). Returns the
|
|
813
|
+
* struct field initializer (`Field: <expr>`). `resolveRef` resolves a ref head for the scalar path.
|
|
814
|
+
*/
|
|
815
|
+
function emitPortInit(pn, expr, fieldGoType, resolveRef) {
|
|
816
|
+
const fieldName = goPortFieldName(pn);
|
|
817
|
+
// change #2: a static string array (projection) → a native `[]string{...}` literal.
|
|
818
|
+
const strArr = staticStringArrElems(expr);
|
|
819
|
+
if (strArr !== null) {
|
|
820
|
+
if (fieldGoType !== "[]string") {
|
|
821
|
+
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).`);
|
|
822
|
+
}
|
|
823
|
+
return `${fieldName}: []string{${strArr.map((s) => JSON.stringify(s)).join(", ")}}`;
|
|
824
|
+
}
|
|
825
|
+
// a bare number literal (e.g. Query `limit: 20`): the SSoT range check ran in numLiteralGoExpr —
|
|
826
|
+
// emit the concrete native int64/float64 (unwrap the dslcontracts.Value(...) box: it was only there
|
|
827
|
+
// for the old boxed path; the range check is what matters, and it already passed).
|
|
828
|
+
const num = numLiteralGoExpr(expr);
|
|
829
|
+
if (num !== null) {
|
|
830
|
+
if (fieldGoType === "int64")
|
|
831
|
+
return `${fieldName}: int64(${expr})`;
|
|
832
|
+
if (fieldGoType === "float64")
|
|
833
|
+
return `${fieldName}: float64(${expr})`;
|
|
834
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90): port '${pn}' is a number literal but its field type is '${fieldGoType}'.`);
|
|
835
|
+
}
|
|
836
|
+
// string / bool / scalar-input / concat → native scalar expression (no Value, no `.(T)`).
|
|
837
|
+
const scalarWant = fieldGoType;
|
|
838
|
+
const native = emitNativeScalar(expr, scalarWant, resolveRef);
|
|
839
|
+
if (native !== null) {
|
|
840
|
+
return `${fieldName}: ${native}`;
|
|
841
|
+
}
|
|
842
|
+
// FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would
|
|
843
|
+
// re-couple the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the
|
|
844
|
+
// component is not native-eligible and must regenerate on the boxed straight-line path.
|
|
845
|
+
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.`);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* emitParallelStageArm (bc#87) — EXPLICIT static parallel orchestration for a real-concurrency stage.
|
|
849
|
+
* The stage's members are statically known (from plan.groups), each a plain componentRef point read.
|
|
850
|
+
* We reproduce plan.go's bounded-parallel protocol EXACTLY, so the observed value / emitted op multiset
|
|
851
|
+
* / failure PRECEDENCE / skip reps byte-match run_behavior:
|
|
852
|
+
*
|
|
853
|
+
* 1. PREFLIGHT (ascending index): for each member, decide run/skip from the SETTLED prior-stage
|
|
854
|
+
* results (a plain `produced_<parent>` gate — members are mutually independent, §1) and build its
|
|
855
|
+
* native ports struct. The ports are computed BEFORE dispatch (never inside the goroutine) so the
|
|
856
|
+
* order-of-evaluation is deterministic and a port ExprFailure surfaces in ascending index order.
|
|
857
|
+
* 2. DISPATCH (bounded): one `go func(){…}()` per RUNNABLE member, bound to the static plan.concurrency
|
|
858
|
+
* via a semaphore channel of known size (min(concurrency, member count)); a sync.WaitGroup joins
|
|
859
|
+
* them. Each goroutine ONLY calls the handler and stores its RawOutcome into a per-member struct
|
|
860
|
+
* local — the goroutine spawn is the SOLE runtime element.
|
|
861
|
+
* 3. COMMIT (ascending index): interpret Policy Kind + materialize the RawValue into the outType
|
|
862
|
+
* struct, in ascending index order. Under 'fail'/'retry', the FIRST (lowest-index) failing member
|
|
863
|
+
* wins — a race can NEVER change which failure surfaces, because interpretation is committed
|
|
864
|
+
* deterministically after all execs settle (collect-then-apply, mirroring plan.go's commit loop).
|
|
865
|
+
*
|
|
866
|
+
* NO RunPlan / []OpSpec / planSpec walk: the staging, the bound, and which ops are parallel are baked.
|
|
867
|
+
*/
|
|
868
|
+
function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags, resolveRefAt) {
|
|
869
|
+
const lines = [];
|
|
870
|
+
const ids = stage.map((i) => comp.body[i].id);
|
|
871
|
+
lines.push(`\t// ── bc#87 real-concurrency stage {${ids.map((x) => JSON.stringify(x)).join(", ")}} — static parallel`);
|
|
872
|
+
lines.push(`\t// orchestration (goroutine per op, bound=${concurrency}); preflight+commit in ascending index order.`);
|
|
873
|
+
lines.push(`\t{`);
|
|
874
|
+
// per-member outcome + resolved-flag + ports locals, declared at STAGE scope (assigned in the
|
|
875
|
+
// ascending preflight, read at dispatch/commit — a goroutine closes over the ports local).
|
|
876
|
+
for (const i of stage) {
|
|
877
|
+
const s = sanitize(comp.body[i].id);
|
|
878
|
+
const structName = portsStructName(comp.name, comp.body[i].id);
|
|
879
|
+
const rowT = rawRowStructName(comp.name, comp.body[i].id);
|
|
880
|
+
lines.push(`\t\tvar row_${s} ${rowT}`);
|
|
881
|
+
lines.push(`\t\tvar row_${s}Resolved bool`);
|
|
882
|
+
lines.push(`\t\trun_${s} := false`);
|
|
883
|
+
lines.push(`\t\tvar ps_${s} ${structName}`);
|
|
884
|
+
lines.push(`\t\t_ = ps_${s}`);
|
|
885
|
+
}
|
|
886
|
+
// 1) PREFLIGHT (ascending): parent-produced gate + ports build, per member (BEFORE dispatch).
|
|
887
|
+
for (const i of stage) {
|
|
888
|
+
const node = comp.body[i];
|
|
889
|
+
const s = sanitize(node.id);
|
|
890
|
+
const structName = portsStructName(comp.name, node.id);
|
|
891
|
+
let ind = "\t\t";
|
|
892
|
+
const closers = [];
|
|
893
|
+
if (node.parent !== undefined && priorNodeAt(node.parent, atPos)) {
|
|
894
|
+
lines.push(`${ind}if produced_${sanitize(node.parent)} {`);
|
|
895
|
+
closers.unshift(`${ind}}`);
|
|
896
|
+
ind += "\t";
|
|
897
|
+
}
|
|
898
|
+
const inits = [];
|
|
899
|
+
const resolveRef = resolveRefAt(atPos);
|
|
900
|
+
for (const pn of Object.keys(node.ports)) {
|
|
901
|
+
const expr = node.ports[pn];
|
|
902
|
+
const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`);
|
|
903
|
+
inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef));
|
|
904
|
+
}
|
|
905
|
+
lines.push(`${ind}ps_${s} = ${structName}{${inits.join(", ")}}`);
|
|
906
|
+
lines.push(`${ind}run_${s} = true`);
|
|
907
|
+
for (const c of closers)
|
|
908
|
+
lines.push(c);
|
|
909
|
+
}
|
|
910
|
+
// 2) DISPATCH (bounded): one goroutine per runnable member, bound by a semaphore of the static size.
|
|
911
|
+
const bound = Math.min(concurrency, stage.length);
|
|
912
|
+
lines.push(`\t\tvar wg sync.WaitGroup`);
|
|
913
|
+
lines.push(`\t\tsem := make(chan struct{}, ${bound})`);
|
|
914
|
+
for (const i of stage) {
|
|
915
|
+
const node = comp.body[i];
|
|
916
|
+
const s = sanitize(node.id);
|
|
917
|
+
lines.push(`\t\tif run_${s} {`);
|
|
918
|
+
lines.push(`\t\t\twg.Add(1)`);
|
|
919
|
+
lines.push(`\t\t\tsem <- struct{}{}`);
|
|
920
|
+
lines.push(`\t\t\tgo func() {`);
|
|
921
|
+
lines.push(`\t\t\t\tdefer wg.Done()`);
|
|
922
|
+
lines.push(`\t\t\t\tdefer func() { <-sem }()`);
|
|
923
|
+
lines.push(`\t\t\t\trow_${s}, row_${s}Resolved = handlers.${nodeMethodName(comp.name, node.id)}(ps_${s}, nil)`);
|
|
924
|
+
lines.push(`\t\t\t}()`);
|
|
925
|
+
lines.push(`\t\t}`);
|
|
926
|
+
}
|
|
927
|
+
lines.push(`\t\twg.Wait()`);
|
|
928
|
+
// 3) COMMIT (ascending index): interpret + materialize deterministically — lowest-index failure wins.
|
|
929
|
+
for (const i of stage) {
|
|
930
|
+
const node = comp.body[i];
|
|
931
|
+
const meta = metas[i];
|
|
932
|
+
const s = sanitize(node.id);
|
|
933
|
+
const ref = typedNodes.get(node.id);
|
|
934
|
+
const oc = `row_${s}`;
|
|
935
|
+
lines.push(`\t\tif run_${s} {`);
|
|
936
|
+
lines.push(`\t\t\tif !${oc}Resolved {`, `\t\t\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${node.component}' has no handler (fail-closed)"`)}`, `\t\t\t}`);
|
|
937
|
+
if (meta.policy === "continue") {
|
|
938
|
+
lines.push(`\t\t\tif !${oc}.IsError {`);
|
|
939
|
+
lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 4));
|
|
940
|
+
lines.push(`\t\t\t\tproduced_${s} = true`);
|
|
941
|
+
lines.push(`\t\t\t}`);
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
const policy = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
|
|
945
|
+
lines.push(`\t\t\tif ${oc}.IsError {`);
|
|
946
|
+
lines.push(`\t\t\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under '${policy}: "+${oc}.Err`)}`);
|
|
947
|
+
lines.push(`\t\t\t}`);
|
|
948
|
+
lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 3));
|
|
949
|
+
lines.push(`\t\t\tproduced_${s} = true`);
|
|
950
|
+
}
|
|
951
|
+
lines.push(`\t\t}`);
|
|
952
|
+
}
|
|
953
|
+
lines.push(`\t}`);
|
|
954
|
+
return lines.join("\n");
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* emitNativeRawRunner — the combined read exec for one component, as a STRUCT-RETURNING runner
|
|
958
|
+
* (the fully de-plumbed path): native ports struct IN (direct field assignment), raw struct result
|
|
959
|
+
* OUT (marshalRaw_* straight into the node's outType struct — no boxed Value on the row plane),
|
|
960
|
+
* INLINE sequential (NO RunPlan). Node results are TYPED STRUCT locals; a relation child reads the
|
|
961
|
+
* parent's REAL struct result inline (typed field access — no serialize), and the child-present /
|
|
962
|
+
* bindField / continue-skip decision is made from the real parent value — so relationSingle /
|
|
963
|
+
* connection converge with run_behavior. The output is assembled as a TYPED STRUCT/value (via the
|
|
964
|
+
* typed output lowering — struct literal + struct-field access, ZERO NewObj/.Set). For a covered
|
|
965
|
+
* read whose ports/output are input-static + cross-node refs, this runner allocates ZERO *Obj: the
|
|
966
|
+
* consumer (graphddb) drives it and keeps the struct native. A Value observation is produced ONLY
|
|
967
|
+
* by the separate serialize wrapper (run_native_raw_<comp>, below), the single outer Value boundary.
|
|
968
|
+
*/
|
|
969
|
+
function emitNativeRawRunner(comp, plan, flags) {
|
|
970
|
+
const ep = execPlan(comp);
|
|
971
|
+
const order = ep.order;
|
|
972
|
+
const cplan = buildComponentPlan(comp, goStraightlineInternals.dialect, "scope");
|
|
973
|
+
const metas = analyzeSequentialOps(cplan.opsLiteral, order);
|
|
974
|
+
const fn = sanitize(comp.name);
|
|
975
|
+
const typedNodes = new Map();
|
|
976
|
+
for (const n of comp.body)
|
|
977
|
+
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
978
|
+
const idToPos = new Map();
|
|
979
|
+
order.forEach((opIdx, k) => idToPos.set(comp.body[opIdx].id, k));
|
|
980
|
+
const priorNodeAt = (head, atPos) => {
|
|
981
|
+
const p = idToPos.get(head);
|
|
982
|
+
return p !== undefined && p < atPos;
|
|
983
|
+
};
|
|
984
|
+
const outRef = comp.outputType !== undefined ? deriveTypeRef(comp.outputType, plan) : undefined;
|
|
985
|
+
const outStructType = outRef ? renderTypeRef(outRef) : `${PKG}.Value`;
|
|
986
|
+
const zeroOut = outRef ? goZero(outRef) : "nil";
|
|
987
|
+
const lines = [];
|
|
988
|
+
lines.push(`// run_native_raw_struct_${fn} — the STRUCT-RETURNING combined read (bc#77/#87): the fully`);
|
|
989
|
+
lines.push(`// de-plumbed path. Ports are a native struct (direct field assignment); the handler`);
|
|
990
|
+
lines.push(`// result is materialized straight into the node's outType struct. Node results are typed`);
|
|
991
|
+
lines.push(`// struct locals; a relation child reads the parent's REAL struct result via direct field`);
|
|
992
|
+
lines.push(`// access (child-present decision from the real parent value — relationSingle / connection`);
|
|
993
|
+
lines.push(`// converge). A strictly-sequential plan is a plain statement list (no plan driver); a`);
|
|
994
|
+
lines.push(`// real-concurrency stage (bc#87) is EXPLICIT static parallel orchestration — one goroutine`);
|
|
995
|
+
lines.push(`// per statically-known parallel op, bounded by the static plan.concurrency via a semaphore`);
|
|
996
|
+
lines.push(`// of known size, with a sync.WaitGroup; preflight + interpret are committed in ascending`);
|
|
997
|
+
lines.push(`// index order so the observed value / op multiset / failure precedence byte-match run_behavior`);
|
|
998
|
+
lines.push(`// (the goroutine spawn is the ONLY runtime element — the WHAT is baked). The output is a typed`);
|
|
999
|
+
lines.push(`// struct/value assembled by struct literal + field access — the consumer keeps it native.`);
|
|
1000
|
+
// FULLY CONCRETE: GENERIC over the per-component CONCRETE handler interface ${handlerIfaceName(comp.name)},
|
|
1001
|
+
// whose Node_* methods return the concrete per-node row struct (typed fields = the node's outType).
|
|
1002
|
+
// The covered result plane carries no boxed ports / boxed value / dynamic accessor / semantic type-
|
|
1003
|
+
// assert on a row value — the runner reads row.<Field> directly and copies each into the outType local.
|
|
1004
|
+
lines.push(`func run_native_raw_struct_${fn}[H ${handlerIfaceName(comp.name)}](handlers H, in ${inStructName(comp.name)}) (${outStructType}, error) {`);
|
|
1005
|
+
lines.push(`\t_ = in`);
|
|
1006
|
+
// resolveNativeRef for this runner: a ref head is either a PRIOR NODE (read its typed struct field
|
|
1007
|
+
// directly) or an INPUT PORT (read the typed `in.<Field>`). A REQUIRED scalar resolves to a native
|
|
1008
|
+
// Go expression; an opt / non-scalar / refOpt resolves to null (falls back to the nvVE Value path).
|
|
1009
|
+
const resolveRefAt = (atPos) => (head, restPath, opt) => {
|
|
1010
|
+
if (opt)
|
|
1011
|
+
return null; // a refOpt may be nil at runtime — keep the Value path (opt-nil semantics)
|
|
1012
|
+
if (priorNodeAt(head, atPos)) {
|
|
1013
|
+
const baseRef = typedNodes.get(head);
|
|
1014
|
+
let acc;
|
|
1015
|
+
let expr;
|
|
1016
|
+
try {
|
|
1017
|
+
const r = goTypedInternals.typedFieldAccess(typedLocal(head), baseRef, restPath, plan);
|
|
1018
|
+
expr = r.expr;
|
|
1019
|
+
acc = r.ref;
|
|
1020
|
+
}
|
|
1021
|
+
catch {
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1025
|
+
return null;
|
|
1026
|
+
return { expr, scalar: goScalar(acc.scalar) };
|
|
1027
|
+
}
|
|
1028
|
+
if (restPath.length !== 0)
|
|
1029
|
+
return null; // input.<field> path is a Value walk — not a native scalar
|
|
1030
|
+
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1031
|
+
if (scalar === undefined)
|
|
1032
|
+
return null;
|
|
1033
|
+
return { expr: `in.${goFieldName(head)}`, scalar };
|
|
1034
|
+
};
|
|
1035
|
+
for (const k of order.keys()) {
|
|
1036
|
+
const id = comp.body[order[k]].id;
|
|
1037
|
+
lines.push(`\tvar ${typedLocal(id)} ${renderTypeRef(typedNodes.get(id))}`);
|
|
1038
|
+
lines.push(`\tproduced_${sanitize(id)} := false`);
|
|
1039
|
+
lines.push(`\t_ = ${typedLocal(id)}`);
|
|
1040
|
+
lines.push(`\t_ = produced_${sanitize(id)}`);
|
|
1041
|
+
}
|
|
1042
|
+
const idToPosAll = new Map();
|
|
1043
|
+
order.forEach((opIdx, k) => idToPosAll.set(opIdx, k));
|
|
1044
|
+
for (let k = 0; k < order.length; k++) {
|
|
1045
|
+
const i = order[k];
|
|
1046
|
+
// bc#87: a real-concurrency stage — emit static parallel orchestration for ALL its members here,
|
|
1047
|
+
// then advance past them. The stage members are consecutive in `order` (execPlan concatenates
|
|
1048
|
+
// ascending-sorted stages), so we emit the block at the first member and skip the rest.
|
|
1049
|
+
const stage = ep.parallelStageOf.get(i);
|
|
1050
|
+
if (stage !== undefined && stage[0] === i) {
|
|
1051
|
+
lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags, resolveRefAt));
|
|
1052
|
+
k += stage.length - 1;
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
// #86 pt2: a covered batched map...into node — struct-native element collection + into enrichment.
|
|
1056
|
+
if ("map" in comp.body[i]) {
|
|
1057
|
+
lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut, flags));
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
const node = comp.body[i];
|
|
1061
|
+
const meta = metas[i];
|
|
1062
|
+
const s = sanitize(node.id);
|
|
1063
|
+
const structName = portsStructName(comp.name, node.id);
|
|
1064
|
+
const portNames = Object.keys(node.ports);
|
|
1065
|
+
const ref = typedNodes.get(node.id);
|
|
1066
|
+
lines.push(`\t// ── op '${node.id}' (${node.component}${node.relationKind ? ", relationKind:" + node.relationKind : ""}${node.parent ? ", parent:" + node.parent : ""}) ──`);
|
|
1067
|
+
let indent = "\t";
|
|
1068
|
+
const closers = [];
|
|
1069
|
+
if (node.parent !== undefined && priorNodeAt(node.parent, k)) {
|
|
1070
|
+
lines.push(`${indent}if produced_${sanitize(node.parent)} {`);
|
|
1071
|
+
closers.unshift(`${indent}}`);
|
|
1072
|
+
indent += "\t";
|
|
1073
|
+
if (node.bindField !== undefined) {
|
|
1074
|
+
// bind (change #3 — typed produced-aware bound): read the parent struct field DIRECTLY (typed
|
|
1075
|
+
// access — no serialize). `bound_<s>` is a TYPED POINTER (`*T`) — nil => UNPRODUCED/skip, non-nil
|
|
1076
|
+
// => produced (an empty string is a VALID produced value, a non-nil *string to ""). This is the
|
|
1077
|
+
// EXACT `!= nil` skip-gate ≡ run_behavior's preflightOp, now runtime-free (NO dslcontracts.Value).
|
|
1078
|
+
const parentRef = typedNodes.get(node.parent);
|
|
1079
|
+
const { expr: bfExpr, ref: bfRef } = goTypedInternals.typedFieldAccess(typedLocal(node.parent), parentRef, [node.bindField], plan);
|
|
1080
|
+
if (bfRef.kind === "opt") {
|
|
1081
|
+
// an opt bindField is ALREADY a *T: nil => skip, non-nil => produced. Use it directly.
|
|
1082
|
+
lines.push(`${indent}bound_${s} := ${bfExpr}`);
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
// a required scalar bindField is always PRESENT (never skips on null): take its address so
|
|
1086
|
+
// the produced pointer is non-nil, preserving the "produced" semantics of the old guard.
|
|
1087
|
+
lines.push(`${indent}boundV_${s} := ${bfExpr}`);
|
|
1088
|
+
lines.push(`${indent}bound_${s} := &boundV_${s}`);
|
|
1089
|
+
}
|
|
1090
|
+
lines.push(`${indent}if bound_${s} != nil {`);
|
|
1091
|
+
closers.unshift(`${indent}}`);
|
|
1092
|
+
indent += "\t";
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// ports: build the native struct by direct field assignment (bc#90 / runtime-free). Every covered
|
|
1096
|
+
// port lowers to a concrete Go value (literal / input-param ref / cross-node typed field / concat /
|
|
1097
|
+
// static string array / number literal) assigned straight into the ports field — ZERO
|
|
1098
|
+
// dslcontracts.Value, ZERO nvVE helper, ZERO `.(T)`. A genuinely-dynamic port FAILS CLOSED.
|
|
1099
|
+
const inits = [];
|
|
1100
|
+
const boundArg = node.bindField !== undefined ? `bound_${s}` : "nil";
|
|
1101
|
+
const resolveRef = resolveRefAt(k);
|
|
1102
|
+
for (const pn of portNames) {
|
|
1103
|
+
const expr = node.ports[pn];
|
|
1104
|
+
const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`);
|
|
1105
|
+
inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef));
|
|
1106
|
+
}
|
|
1107
|
+
lines.push(`${indent}ports_${s} := ${structName}{${inits.join(", ")}}`);
|
|
1108
|
+
const oc = `row_${s}`;
|
|
1109
|
+
lines.push(`${indent}${oc}, ${oc}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg})`);
|
|
1110
|
+
lines.push(`${indent}if !${oc}Resolved {`, `${indent}\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${node.component}' has no handler (fail-closed)"`)}`, `${indent}}`);
|
|
1111
|
+
const copy = (ind) => emitRowCopy(typedLocal(node.id), oc, ref, plan, ind.length);
|
|
1112
|
+
if (meta.policy === "continue") {
|
|
1113
|
+
lines.push(`${indent}if !${oc}.IsError {`);
|
|
1114
|
+
lines.push(copy(indent + "\t"));
|
|
1115
|
+
lines.push(`${indent}\tproduced_${s} = true`);
|
|
1116
|
+
lines.push(`${indent}}`);
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
const policy = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
|
|
1120
|
+
lines.push(`${indent}if ${oc}.IsError {`);
|
|
1121
|
+
lines.push(`${indent}\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under '${policy}: "+${oc}.Err`)}`);
|
|
1122
|
+
lines.push(`${indent}}`);
|
|
1123
|
+
lines.push(copy(indent));
|
|
1124
|
+
lines.push(`${indent}produced_${s} = true`);
|
|
1125
|
+
}
|
|
1126
|
+
for (const c of closers)
|
|
1127
|
+
lines.push(c);
|
|
1128
|
+
}
|
|
1129
|
+
// output: assemble a TYPED struct/value via the typed output lowering (struct literal + typed
|
|
1130
|
+
// field access — ZERO NewObj/.Set). This is the reused boxed-typed output assembly, but the
|
|
1131
|
+
// result stays a struct (the consumer keeps it native). An input-param output ref would need the
|
|
1132
|
+
// Value-scope fallback (scopeNR) — the covered read corpus does not hit it.
|
|
1133
|
+
//
|
|
1134
|
+
// #86 part 1: for a bindField relationSingle child that can SKIP at runtime, a ref/refOpt into it
|
|
1135
|
+
// must yield the UNPRODUCED rep (run_behavior's writeUnproduced: null for a single child) rather
|
|
1136
|
+
// than the zero-value struct. So when any covered node canSkip, we use the produced-aware lowering
|
|
1137
|
+
// which gates such a ref on the node's produced_<id> flag.
|
|
1138
|
+
const skippable = new Set();
|
|
1139
|
+
const skippableKind = new Map();
|
|
1140
|
+
for (const k of order.keys()) {
|
|
1141
|
+
const bodyNode = comp.body[order[k]];
|
|
1142
|
+
const id = bodyNode.id;
|
|
1143
|
+
if (metas[order[k]].canSkip) {
|
|
1144
|
+
skippable.add(id);
|
|
1145
|
+
// relationKind of the skippable node decides its unproduced rep (#93 shape 1): a connection
|
|
1146
|
+
// yields the empty connection {items:[],cursor:null}; single/absent yields null (opt-nil).
|
|
1147
|
+
const rk = "map" in bodyNode ? bodyNode.map.relationKind : bodyNode.relationKind;
|
|
1148
|
+
skippableKind.set(id, rk);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
const out = skippable.size > 0
|
|
1152
|
+
? emitProducedAwareValue(comp.output, outRef, typedNodes, plan, skippable, skippableKind)
|
|
1153
|
+
: emitTypedValue(comp.output, outRef, typedNodes, plan);
|
|
1154
|
+
lines.push(`\treturn ${out.expr}, nil`);
|
|
1155
|
+
lines.push(`}`);
|
|
1156
|
+
return { code: lines.join("\n"), outRef: out.ref ?? outRef };
|
|
1157
|
+
}
|
|
1158
|
+
// bc#90 / runtime-free: the old nvVE-based `emitMapStaticVE` map-element port lowering is REMOVED —
|
|
1159
|
+
// covered map element ports lower to a fully native Go value in emitPortInit (the map resolveRef reads
|
|
1160
|
+
// the $as element / prior-node / input typed fields directly), with a fail-closed on a dynamic port.
|
|
1161
|
+
/**
|
|
1162
|
+
* emitMapArm — the struct-native exec of a covered map node. Three shapes:
|
|
1163
|
+
* - #86 pt2: BATCHED map...into (nestedBatchGet) — dispatch once, zip-attach `into` onto each over
|
|
1164
|
+
* element (the augmented element materializer).
|
|
1165
|
+
* - #93 shape 2: NON-batched map...into — dispatch the child handler ONCE PER ELEMENT (N physical
|
|
1166
|
+
* requests, matching run_behavior's per-element loop), materialize `into` per element.
|
|
1167
|
+
* - #93 shape 3: BATCHED map with NO `into` — dispatch once, collect each element DIRECTLY from the
|
|
1168
|
+
* aligned batch raw via the element's own named marshaller (the connection/plain-list collect).
|
|
1169
|
+
* In every case: over is a typed field access (no serialize), element ports are a native struct
|
|
1170
|
+
* (direct field assignment; $as refs read the over element's typed fields), and the result is a
|
|
1171
|
+
* native []T with NO boxed Value on any plane. The map is under the parent's produced gate (inline).
|
|
1172
|
+
*/
|
|
1173
|
+
function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, flags) {
|
|
1174
|
+
const m = node.map;
|
|
1175
|
+
const batched = m.batched === true;
|
|
1176
|
+
const hasInto = m.into !== undefined;
|
|
1177
|
+
const s = sanitize(node.id);
|
|
1178
|
+
const structName = portsStructName(comp.name, node.id);
|
|
1179
|
+
const batchStruct = `${structName}_batch`;
|
|
1180
|
+
const arrRef = typedNodes.get(node.id);
|
|
1181
|
+
const elemGo = renderTypeRef(arrRef);
|
|
1182
|
+
const asName = m.as;
|
|
1183
|
+
const overElemGo = mapOverElemType(comp, node, typedNodes, plan);
|
|
1184
|
+
// over element type ref (for $as field access): resolve the parent field's arr element type.
|
|
1185
|
+
const overE = classifyExpr(m.over);
|
|
1186
|
+
const overParent = overE.kind === "ref" ? overE.path[0] : "";
|
|
1187
|
+
const overBaseRef = typedNodes.get(overParent);
|
|
1188
|
+
const { expr: overExpr, ref: overRef } = goTypedInternals.typedFieldAccess(typedLocal(overParent), overBaseRef, overE.kind === "ref" ? overE.path.slice(1) : [], plan);
|
|
1189
|
+
const overElemRef = overRef.elem;
|
|
1190
|
+
const elemMat = mapElemMaterializerName(comp.name, node.id);
|
|
1191
|
+
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1192
|
+
const portNames = Object.keys(m.ports);
|
|
1193
|
+
// emit the per-element native ports struct build (shared; `il` is the indent inside the element
|
|
1194
|
+
// loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
|
|
1195
|
+
const buildPorts = (il, elemVar) => {
|
|
1196
|
+
const out = [];
|
|
1197
|
+
const inits = [];
|
|
1198
|
+
// resolveNativeRef for a map element port: a ref head is the element binding ($as → the over element
|
|
1199
|
+
// typed struct field `elemVar.<Field>`), a PRIOR NODE (its typed struct field), or an INPUT PORT
|
|
1200
|
+
// (`in.<Field>`). A REQUIRED scalar lowers to a native Go expr; opt / non-scalar → null (Value path).
|
|
1201
|
+
const resolveRef = (head, restPath, opt) => {
|
|
1202
|
+
if (opt)
|
|
1203
|
+
return null;
|
|
1204
|
+
let baseExpr;
|
|
1205
|
+
let baseRef;
|
|
1206
|
+
if (head === asName) {
|
|
1207
|
+
baseExpr = elemVar;
|
|
1208
|
+
baseRef = overElemRef;
|
|
1209
|
+
}
|
|
1210
|
+
else if (priorHere(head)) {
|
|
1211
|
+
baseExpr = typedLocal(head);
|
|
1212
|
+
baseRef = typedNodes.get(head);
|
|
1213
|
+
}
|
|
1214
|
+
else {
|
|
1215
|
+
if (restPath.length !== 0)
|
|
1216
|
+
return null;
|
|
1217
|
+
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1218
|
+
if (scalar === undefined)
|
|
1219
|
+
return null;
|
|
1220
|
+
return { expr: `in.${goFieldName(head)}`, scalar };
|
|
1221
|
+
}
|
|
1222
|
+
let acc;
|
|
1223
|
+
let expr;
|
|
1224
|
+
try {
|
|
1225
|
+
const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
|
|
1226
|
+
expr = r.expr;
|
|
1227
|
+
acc = r.ref;
|
|
1228
|
+
}
|
|
1229
|
+
catch {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1233
|
+
return null;
|
|
1234
|
+
return { expr, scalar: goScalar(acc.scalar) };
|
|
1235
|
+
};
|
|
1236
|
+
for (const pn of portNames) {
|
|
1237
|
+
const fieldGoType = portFieldGoType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`);
|
|
1238
|
+
inits.push(emitPortInit(pn, m.ports[pn], fieldGoType, resolveRef));
|
|
1239
|
+
}
|
|
1240
|
+
out.push(`${il}ep_${s} := ${structName}{${inits.join(", ")}}`);
|
|
1241
|
+
return out;
|
|
1242
|
+
};
|
|
1243
|
+
const lines = [];
|
|
1244
|
+
const closers = [];
|
|
1245
|
+
lines.push(`\t// ── map '${node.id}' (${m.component}, ${batched ? "batched" : "per-element"}, into:${JSON.stringify(m.into ?? null)}${m.relationKind ? ", relationKind:" + m.relationKind : ""}${m.parent ? ", parent:" + m.parent : ""}) ──`);
|
|
1246
|
+
let indent = "\t";
|
|
1247
|
+
// the map is gated on the parent's produced flag (inline, like a relation child).
|
|
1248
|
+
if (m.parent !== undefined && priorNodeAt(m.parent, atPos)) {
|
|
1249
|
+
lines.push(`${indent}if produced_${sanitize(m.parent)} {`);
|
|
1250
|
+
closers.unshift(`${indent}}`);
|
|
1251
|
+
indent += "\t";
|
|
1252
|
+
}
|
|
1253
|
+
// over collection (typed field access to the parent node's arr — no serialize).
|
|
1254
|
+
lines.push(`${indent}over_${s} := ${overExpr}`);
|
|
1255
|
+
lines.push(`${indent}${typedLocal(node.id)} = make(${elemGo}, 0, len(over_${s}))`);
|
|
1256
|
+
const elemRowRef = mapElemRowRef(node, arrRef, plan);
|
|
1257
|
+
if (batched) {
|
|
1258
|
+
// BATCHED (#86 pt2 into / #93 shape 3 no-into): build all ports, dispatch ONCE. The concrete
|
|
1259
|
+
// Node_* returns RawRow_<comp>_<node>{IsError, Err, Rows []RawElem_} aligned to the over items.
|
|
1260
|
+
lines.push(`${indent}items_${s} := make([]${structName}, 0, len(over_${s}))`);
|
|
1261
|
+
lines.push(`${indent}for _, ${elemLocal(s)} := range over_${s} {`);
|
|
1262
|
+
for (const l of buildPorts(indent + "\t", elemLocal(s)))
|
|
1263
|
+
lines.push(l);
|
|
1264
|
+
lines.push(`${indent}\titems_${s} = append(items_${s}, ep_${s})`);
|
|
1265
|
+
lines.push(`${indent}}`);
|
|
1266
|
+
// empty over => empty result (handler not called), matching run_behavior.
|
|
1267
|
+
lines.push(`${indent}if len(items_${s}) > 0 {`);
|
|
1268
|
+
lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: items_${s}}`);
|
|
1269
|
+
lines.push(`${indent}\tmo_${s}, mo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
|
|
1270
|
+
lines.push(`${indent}\tif !mo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${m.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
|
|
1271
|
+
lines.push(`${indent}\tif mo_${s}.IsError {`, `${indent}\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under 'fail' policy: "+mo_${s}.Err`)}`, `${indent}\t}`);
|
|
1272
|
+
lines.push(`${indent}\tif len(mo_${s}.Rows) != len(items_${s}) {`, `${indent}\t\treturn ${zeroOut}, ${goErr("MAP_BATCH_RESULT_MISMATCH", `fmt.Sprintf("map '${node.id}': batched handler must return a list aligned to items (want %d)", len(items_${s}))`)}`, `${indent}\t}`);
|
|
1273
|
+
lines.push(`${indent}\tfor mk_${s} := range over_${s} {`);
|
|
1274
|
+
if (hasInto) {
|
|
1275
|
+
// #86 pt2: zip-attach the into field onto each over element (augmented element copier).
|
|
1276
|
+
lines.push(`${indent}\t\tel_${s} := ${elemMat}(over_${s}[mk_${s}], mo_${s}.Rows[mk_${s}])`);
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
// #93 shape 3: collect each element DIRECTLY from the concrete per-element row (field copy).
|
|
1280
|
+
lines.push(`${indent}\t\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
|
|
1281
|
+
lines.push(emitRowCopy(`el_${s}`, `mo_${s}.Rows[mk_${s}]`, elemRowRef, plan, indent.length + 2));
|
|
1282
|
+
}
|
|
1283
|
+
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1284
|
+
lines.push(`${indent}\t}`);
|
|
1285
|
+
lines.push(`${indent}}`);
|
|
1286
|
+
}
|
|
1287
|
+
else {
|
|
1288
|
+
// #93 shape 2: NON-batched map...into — dispatch the child handler ONCE PER ELEMENT (N physical
|
|
1289
|
+
// requests, matching run_behavior's per-element loop). The concrete Node_* returns ONE RawElem_.
|
|
1290
|
+
lines.push(`${indent}for mk_${s} := range over_${s} {`);
|
|
1291
|
+
lines.push(`${indent}\t${elemLocal(s)} := over_${s}[mk_${s}]`);
|
|
1292
|
+
for (const l of buildPorts(indent + "\t", elemLocal(s)))
|
|
1293
|
+
lines.push(l);
|
|
1294
|
+
// per-element dispatch (ONE physical request per element). run_behavior passes the over element
|
|
1295
|
+
// as the bound value; the element data is fully conveyed by the native ports struct here, so bound
|
|
1296
|
+
// stays nil. Byte-equivalence holds because the bound value only feeds handler-internal logic.
|
|
1297
|
+
lines.push(`${indent}\tmo_${s}, mo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(ep_${s}, nil)`);
|
|
1298
|
+
lines.push(`${indent}\tif !mo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${m.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
|
|
1299
|
+
lines.push(`${indent}\tif mo_${s}.IsError {`, `${indent}\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under 'fail' policy: "+mo_${s}.Err`)}`, `${indent}\t}`);
|
|
1300
|
+
if (hasInto) {
|
|
1301
|
+
lines.push(`${indent}\tel_${s} := ${elemMat}(${elemLocal(s)}, mo_${s})`);
|
|
1302
|
+
}
|
|
1303
|
+
else {
|
|
1304
|
+
lines.push(`${indent}\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
|
|
1305
|
+
lines.push(emitRowCopy(`el_${s}`, `mo_${s}`, elemRowRef, plan, indent.length + 1));
|
|
1306
|
+
}
|
|
1307
|
+
lines.push(`${indent}\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1308
|
+
lines.push(`${indent}}`);
|
|
1309
|
+
}
|
|
1310
|
+
lines.push(`${indent}produced_${s} = true`);
|
|
1311
|
+
for (const c of closers)
|
|
1312
|
+
lines.push(c);
|
|
1313
|
+
void overElemGo;
|
|
1314
|
+
return lines.join("\n");
|
|
1315
|
+
}
|
|
1316
|
+
/** the map element loop variable (the over element, a typed struct). */
|
|
1317
|
+
function elemLocal(s) {
|
|
1318
|
+
return `oel_${s}`;
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* emitProducedAwareValue — the #86 part 1 produced-aware output lowering (go). It mirrors the
|
|
1322
|
+
* boxed-typed emitTypedValue, but a `ref`/`refOpt` whose HEAD is a skippable relationSingle child is
|
|
1323
|
+
* gated on that node's `produced_<id>` flag: when the child is UNPRODUCED (bound parent field was
|
|
1324
|
+
* null/absent), run_behavior's writeUnproduced makes the node null, so a refOpt into it yields null
|
|
1325
|
+
* (the output opt field => nil). We emit that exactly: `if produced_<head> { <typed field access> }
|
|
1326
|
+
* else { <unproduced-rep for the expected type> }`. Obj/arr/concat/literal recurse; a ref into a
|
|
1327
|
+
* NON-skippable node delegates to the plain typed field access (identical to emitTypedValue).
|
|
1328
|
+
*/
|
|
1329
|
+
function emitProducedAwareValue(node, expected, typedNodes, plan, skippable, skippableKind) {
|
|
1330
|
+
// ref / refOpt (classifyExpr encodes both as kind:"ref" with .opt).
|
|
1331
|
+
const e = classifyExpr(node);
|
|
1332
|
+
if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
|
|
1333
|
+
const head = e.path[0];
|
|
1334
|
+
const baseRef = typedNodes.get(head);
|
|
1335
|
+
if (skippable.has(head)) {
|
|
1336
|
+
// produced-aware: gate the field access on produced_<head>. The unproduced branch yields the
|
|
1337
|
+
// node's unproduced rep per run_behavior's writeUnproduced (unproducedValue(relationKind)):
|
|
1338
|
+
// - single / absent → null (opt-nil);
|
|
1339
|
+
// - connection (#93 shape 1) → the empty connection {items:[],cursor:null}. Because a WHOLE
|
|
1340
|
+
// connection node has no field path (e.path.length===1), the unproduced rep is the struct
|
|
1341
|
+
// zero of the connection struct (its serializer normalizes a nil items slice to [] and the
|
|
1342
|
+
// nil cursor to null) — matching unproducedValue("connection") byte-for-byte.
|
|
1343
|
+
const { expr, ref } = goTypedInternals.typedFieldAccess(typedLocal(head), baseRef, e.path.slice(1), plan);
|
|
1344
|
+
const producedFlag = `produced_${sanitize(head)}`;
|
|
1345
|
+
const producedExpr = valueLeafTyped(expr, ref, e.opt, expected);
|
|
1346
|
+
const outTy = expected ? renderTypeRef(expected) : renderTypeRef(ref);
|
|
1347
|
+
const isConn = skippableKind.get(head) === "connection" && e.path.length === 1;
|
|
1348
|
+
// For a connection whole-node ref, the unproduced rep is the connection struct zero (empty
|
|
1349
|
+
// items + nil cursor), even if the output field is opt (refOpt): run_behavior always writes the
|
|
1350
|
+
// empty connection, never null. For single/field-access, the expected-type zero (nil for opt).
|
|
1351
|
+
const unproduced = isConn ? goConnectionUnproduced(ref, expected) : goZero(expected ?? ref);
|
|
1352
|
+
return {
|
|
1353
|
+
expr: `func() ${outTy} { if ${producedFlag} { return ${producedExpr} }; return ${unproduced} }()`,
|
|
1354
|
+
ref: expected ?? ref,
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
// non-skippable node ref — identical to emitTypedValue's typed field access.
|
|
1358
|
+
const { expr, ref } = goTypedInternals.typedFieldAccess(typedLocal(head), baseRef, e.path.slice(1), plan);
|
|
1359
|
+
return { expr, ref };
|
|
1360
|
+
}
|
|
1361
|
+
// obj-assembly: recurse each field with the produced-aware lowering (the struct fields may each
|
|
1362
|
+
// ref a skippable node). Only handle the covered named-struct obj output.
|
|
1363
|
+
if (node !== null && typeof node === "object" && !Array.isArray(node)) {
|
|
1364
|
+
const keys = Object.keys(node);
|
|
1365
|
+
if (keys.length === 1 && keys[0] === "obj" && expected && expected.kind === "named") {
|
|
1366
|
+
const decl = findDecl(plan, expected.name);
|
|
1367
|
+
const fields = node.obj;
|
|
1368
|
+
const inits = decl.fields.map((f) => {
|
|
1369
|
+
const sub = emitProducedAwareValue(fields[f.name], f.type, typedNodes, plan, skippable, skippableKind);
|
|
1370
|
+
return `${goFieldName(f.name)}: ${sub.expr}`;
|
|
1371
|
+
});
|
|
1372
|
+
return { expr: `${expected.name}{${inits.join(", ")}}`, ref: expected };
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
// everything else (literal / non-skippable ref / obj-of-non-named / arr / concat) — delegate to
|
|
1376
|
+
// the plain typed lowering (no skippable head reachable there in the covered corpus).
|
|
1377
|
+
return emitTypedValue(node, expected, typedNodes, plan);
|
|
1378
|
+
}
|
|
1379
|
+
/** Lower a typed field-access expr to the EXPECTED output type (used in the produced branch). When
|
|
1380
|
+
* the expected type is `opt` and the accessed value is already a *T pointer (the field itself is
|
|
1381
|
+
* opt), pass it through; when the accessed value is a bare leaf and expected is opt, take its addr. */
|
|
1382
|
+
function valueLeafTyped(expr, ref, opt, expected) {
|
|
1383
|
+
void opt;
|
|
1384
|
+
if (expected && expected.kind === "opt") {
|
|
1385
|
+
if (ref.kind === "opt")
|
|
1386
|
+
return expr; // *T already
|
|
1387
|
+
// wrap a bare value in a fresh pointer (matches renderTypeRef(opt)=*T).
|
|
1388
|
+
return `func() ${renderTypeRef(expected)} { __v := ${expr}; return &__v }()`;
|
|
1389
|
+
}
|
|
1390
|
+
return expr;
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* goConnectionUnproduced — the unproduced rep for a SKIPPED relationKind:connection node (#93 shape
|
|
1394
|
+
* 1): the empty connection {items:[],cursor:null}. The connection outType is a named struct whose
|
|
1395
|
+
* serializer normalizes a nil items slice to [] and a nil cursor to null, so the struct ZERO
|
|
1396
|
+
* (`Tn{}`) serializes to exactly unproducedValue("connection"). When the output field is opt
|
|
1397
|
+
* (refOpt into the connection), run_behavior still writes the empty connection (never null), so we
|
|
1398
|
+
* wrap the struct zero in a fresh pointer rather than yielding nil.
|
|
1399
|
+
*/
|
|
1400
|
+
function goConnectionUnproduced(nodeRef, expected) {
|
|
1401
|
+
const connRef = expected && expected.kind === "opt" ? expected.inner : (expected ?? nodeRef);
|
|
1402
|
+
const zero = goZero(connRef);
|
|
1403
|
+
if (expected && expected.kind === "opt") {
|
|
1404
|
+
return `func() ${renderTypeRef(expected)} { __c := ${zero}; return &__c }()`;
|
|
1405
|
+
}
|
|
1406
|
+
return zero;
|
|
1407
|
+
}
|
|
1408
|
+
/** Go zero value for a TypeRef (used as the error return of the struct runner). */
|
|
1409
|
+
function goZero(ref) {
|
|
1410
|
+
switch (ref.kind) {
|
|
1411
|
+
case "named":
|
|
1412
|
+
return `${ref.name}{}`;
|
|
1413
|
+
case "scalar":
|
|
1414
|
+
switch (ref.scalar) {
|
|
1415
|
+
case "string":
|
|
1416
|
+
return `""`;
|
|
1417
|
+
case "int":
|
|
1418
|
+
return "0";
|
|
1419
|
+
case "float":
|
|
1420
|
+
return "0";
|
|
1421
|
+
case "bool":
|
|
1422
|
+
return "false";
|
|
1423
|
+
case "null":
|
|
1424
|
+
return "nil";
|
|
1425
|
+
}
|
|
1426
|
+
return "nil";
|
|
1427
|
+
case "arr":
|
|
1428
|
+
return "nil";
|
|
1429
|
+
case "opt":
|
|
1430
|
+
return "nil";
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
// bc#90 / runtime-free: the nv* helper set (nvVE/nvBindVE/nvFieldVE/nvCatVE/nvArrVE) is REMOVED —
|
|
1434
|
+
// those were dslcontracts.Value-based; the covered plane now lowers every port to a native Go value
|
|
1435
|
+
// (emitPortInit) so no VE helper is emitted, and the module imports ZERO bc runtime.
|
|
1436
|
+
// ── BindNativeRaw dispatch surface ──────────────────────────────────────────────────────
|
|
1437
|
+
/** The struct dispatch surface: ComponentNamesNativeRaw + the per-component struct runners
|
|
1438
|
+
* (run_native_raw_struct_<comp>) ARE the exposed API. The consumer calls the struct runner directly
|
|
1439
|
+
* (it returns the typed model — NO Value serialization on the read hot path, matching how graphddb
|
|
1440
|
+
* reads: a model, never a boxed Value). There is deliberately NO Value dispatch map — that would
|
|
1441
|
+
* require a Value serializer (the boxing residue bc#77 eliminates). A canonical Value observation
|
|
1442
|
+
* (equivalence pin) is produced by the SEPARATE test-only observe companion (goTypedNativeObserve),
|
|
1443
|
+
* which lives OUTSIDE this module so the covered module stays literal whole-file zero. */
|
|
1444
|
+
function emitStructSurface(native) {
|
|
1445
|
+
return `// ComponentNamesNativeRaw — covered reads exposed on the combined struct-native path (declaration
|
|
1446
|
+
// order). Each is driven via run_native_raw_struct_<comp>(handlers, input) -> (T, error): a STRUCT
|
|
1447
|
+
// return; the consumer keeps the model native (no Value serialization on the read hot path — the
|
|
1448
|
+
// boxing residue bc#77 removes). See INTEGRATION.md §6.
|
|
1449
|
+
var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name)).join(", ")}}`;
|
|
1450
|
+
}
|
|
1451
|
+
/** Minimal module header for a FULLY-covered module (bc#90 / runtime-free). A fully-covered module
|
|
1452
|
+
* imports ZERO bc runtime (only the std-lib packages it genuinely uses: `fmt` for a batched-map
|
|
1453
|
+
* mismatch Sprintf, `strings` for a concat builder, `sync` for a parallel stage). ExpectedSpecVersions
|
|
1454
|
+
* stays as a local `var` for provenance, but the old `dslcontracts.SpecVersions` skew `init()` is GONE
|
|
1455
|
+
* (change #5 — there is no runtime to skew against once the module is runtime-free). */
|
|
1456
|
+
function emitMinimalHeader(ctx, needStrings, needSync, needFmt) {
|
|
1457
|
+
const sv = ctx.specVersions;
|
|
1458
|
+
const imports = [];
|
|
1459
|
+
if (needFmt)
|
|
1460
|
+
imports.push(`\t"fmt"`);
|
|
1461
|
+
if (needStrings)
|
|
1462
|
+
imports.push(`\t"strings"`);
|
|
1463
|
+
if (needSync)
|
|
1464
|
+
imports.push(`\t"sync"`);
|
|
1465
|
+
const importBlock = imports.length > 0 ? `\nimport (\n${imports.join("\n")}\n)\n` : "";
|
|
1466
|
+
return `// GENERATED by behavior-contracts (bc#90, go-typed-native — the RUNTIME-FREE covered read de-box) — DO NOT EDIT.
|
|
1467
|
+
// Input: portable component-graph IR only. Every component here is a COVERED READ (sequential typed
|
|
1468
|
+
// componentRef); it is emitted ONLY as the combined native ports IN + raw struct OUT + inline
|
|
1469
|
+
// sequential runner below. There is NO generic Bind/dispatch/plan scaffolding AND NO bc-runtime import —
|
|
1470
|
+
// a fully-covered module is FULLY NATIVE (zero bc-runtime reference; only the std-lib it uses).
|
|
1471
|
+
|
|
1472
|
+
package behaviors
|
|
1473
|
+
${importBlock}
|
|
1474
|
+
// ExpectedSpecVersions are the spec versions baked at generation time (provenance only — the module
|
|
1475
|
+
// is runtime-free, so there is no runtime SpecVersions to skew-check against, bc#90 / change #5).
|
|
1476
|
+
var ExpectedSpecVersions = map[string]int64{"behavior": ${sv.behavior}, "expression": ${sv.expression}, "plan": ${sv.plan}}`;
|
|
1477
|
+
}
|
|
1478
|
+
// ── module assembly ──────────────────────────────────────────────────────────────────
|
|
1479
|
+
function emit(ctx) {
|
|
1480
|
+
// Fail closed if a covered read SHAPE lacks the typed annotations (no silent re-box).
|
|
1481
|
+
for (const c of ctx.ir.components)
|
|
1482
|
+
assertTypedCoverage(c);
|
|
1483
|
+
const native = ctx.ir.components.filter(isNativeRaw);
|
|
1484
|
+
const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
|
|
1485
|
+
const plan = buildTypePlan(ctx.ir);
|
|
1486
|
+
const decls = emitTypeDecls(plan);
|
|
1487
|
+
// Emit the runners first so the nv* helper usage flags are known.
|
|
1488
|
+
const flags = { bind: false, field: false, cat: false, arr: false };
|
|
1489
|
+
const structs = [];
|
|
1490
|
+
const inStructs = [];
|
|
1491
|
+
const rowStructs = [];
|
|
1492
|
+
const handlerIfaces = [];
|
|
1493
|
+
const elemCopiers = [];
|
|
1494
|
+
const runnerCodes = [];
|
|
1495
|
+
for (const c of native) {
|
|
1496
|
+
inStructs.push(emitInStruct(c));
|
|
1497
|
+
const typedNodes = new Map();
|
|
1498
|
+
for (const n of c.body) {
|
|
1499
|
+
if ("map" in n) {
|
|
1500
|
+
// #86 pt2: a map node emits an element ports struct + a batch struct (Items []Value of the
|
|
1501
|
+
// per-element native ports structs; no key-value object in the accessor).
|
|
1502
|
+
structs.push(emitMapPortsStructs(c, n));
|
|
1503
|
+
}
|
|
1504
|
+
else {
|
|
1505
|
+
structs.push(emitPortsStruct(c, n));
|
|
1506
|
+
}
|
|
1507
|
+
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1508
|
+
}
|
|
1509
|
+
// CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
|
|
1510
|
+
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
1511
|
+
handlerIfaces.push(emitHandlerInterface(c, typedNodes, plan));
|
|
1512
|
+
// augmented-element copiers for covered map...into nodes (over fields + concrete into row).
|
|
1513
|
+
const mm = emitMapElemMaterializers(c, typedNodes, plan);
|
|
1514
|
+
if (mm)
|
|
1515
|
+
elemCopiers.push(mm);
|
|
1516
|
+
const r = emitNativeRawRunner(c, plan, flags);
|
|
1517
|
+
runnerCodes.push(r.code);
|
|
1518
|
+
}
|
|
1519
|
+
const structSurface = emitStructSurface(native);
|
|
1520
|
+
const behaviorErrorType = emitBehaviorErrorType();
|
|
1521
|
+
// bc#87: a covered component with a real-concurrency stage emits sync.WaitGroup + a semaphore.
|
|
1522
|
+
const needSync = native.some((c) => concurrencyPlan(c) !== null);
|
|
1523
|
+
// bc#90: the ONLY std-lib `fmt.Sprintf` site on the covered plane is the batched-map result-count
|
|
1524
|
+
// mismatch error — so `fmt` is imported iff a covered component has a batched map node.
|
|
1525
|
+
const needFmt = native.some((c) => c.body.some((n) => "map" in n && n.map.batched === true));
|
|
1526
|
+
// The FULLY-covered module (no non-native components) gets a MINIMAL, RUNTIME-FREE header — NO
|
|
1527
|
+
// bc-runtime import at all (bc#90).
|
|
1528
|
+
// FAIL CLOSED (bc#86/#89): the --typed-native (native codegen) surface is native-only. If ANY
|
|
1529
|
+
// component is not a covered native read, we do NOT silently box it through the straight-line
|
|
1530
|
+
// emitter (the old concealment) — we throw a LOUD GeneratorFailure naming every uncovered component
|
|
1531
|
+
// and its specific reject reason. A fully-covered module (nonNative.length === 0) is unchanged: the
|
|
1532
|
+
// minimal, runtime-free header.
|
|
1533
|
+
if (nonNative.length > 0) {
|
|
1534
|
+
const rejects = nonNative
|
|
1535
|
+
.map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
|
|
1536
|
+
.join("\n");
|
|
1537
|
+
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` +
|
|
1538
|
+
`The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
|
|
1539
|
+
`box uncovered shapes. Implement native coverage for this shape, fix the spec if the shape is ` +
|
|
1540
|
+
`genuinely runtime-dynamic, or generate this component on the --ir dynamic interpreter surface.`);
|
|
1541
|
+
}
|
|
1542
|
+
const header = emitMinimalHeader(ctx, false, needSync, needFmt);
|
|
1543
|
+
const banner = `// COMBINED READ layer (bc#90 — the RUNTIME-FREE read de-box; STRUCT-ONLY, FULLY-NATIVE surface). A
|
|
1544
|
+
// covered read is a strictly-sequential typed componentRef chain (point reads + relationKind:single
|
|
1545
|
+
// children), emitted ONLY here: each node builds a native ports struct by direct field assignment (every
|
|
1546
|
+
// port is a CONCRETE Go value — string / bool / []string projection / int64 limit — NO boxed Value, NO
|
|
1547
|
+
// PortReader accessor); the CONCRETE per-component handler interface (Handler_<comp>) takes a typed
|
|
1548
|
+
// produced-aware 'bound' pointer and returns a CONCRETE per-node row struct (Row_<comp>_<node>, typed
|
|
1549
|
+
// fields = the node's outType) whose fields the runner reads DIRECTLY and copies into the node's outType
|
|
1550
|
+
// struct. The covered plane carries NO bc-runtime reference at all — failures use the LOCAL
|
|
1551
|
+
// *BehaviorError (same codes, byte-equal to run_behavior). Exec is INLINE sequential (no plan driver) so
|
|
1552
|
+
// a relation child reads the parent's REAL struct result via direct field access and the child-present
|
|
1553
|
+
// decision is made from the real parent value — relationSingle / connection converge with run_behavior
|
|
1554
|
+
// (fixes #323/#74). The exposed API is the STRUCT-returning run_native_raw_struct_<comp> (the consumer
|
|
1555
|
+
// keeps the model native — no serialization on the read hot path). This module is FULLY NATIVE: a naive
|
|
1556
|
+
// grep for boxing primitives OR for the bc-runtime package finds nothing, by design (bc#90/runtime-free).`;
|
|
1557
|
+
const sections = [
|
|
1558
|
+
banner,
|
|
1559
|
+
`// Local concrete failure type (runtime-free) — a covered runner returns *BehaviorError (satisfies\n// error) instead of a bc-runtime failure, so the fully-covered module imports ZERO bc runtime.\n${behaviorErrorType}`,
|
|
1560
|
+
decls ? `// Typed struct declarations (outType-derived; hash-dedup — shared type plan).\n${decls}` : undefined,
|
|
1561
|
+
`// Per-component CONCRETE input structs (fields = inputPorts; the consumer builds them natively —\n// no generic *Obj crosses the covered read boundary).\n${inStructs.join("\n\n")}`,
|
|
1562
|
+
`// CONCRETE per-node handler-result row structs (typed fields = the node's outType; + error signal).\n${rowStructs.join("\n\n")}`,
|
|
1563
|
+
`// Per-component CONCRETE handler interfaces (one typed Node_* method per covered node).\n${handlerIfaces.join("\n\n")}`,
|
|
1564
|
+
elemCopiers.length
|
|
1565
|
+
? `// Augmented map-element copiers (over element fields + the concrete into row — pure assignment).\n${elemCopiers.join("\n\n")}`
|
|
1566
|
+
: undefined,
|
|
1567
|
+
`// Native ports structs (one per componentRef node; typed per the static port type).\n${structs.join("\n\n")}`,
|
|
1568
|
+
`// Combined read runners (STRUCT-returning — the fully de-plumbed path).\n${runnerCodes.join("\n\n")}`,
|
|
1569
|
+
structSurface,
|
|
1570
|
+
].filter((s) => !!s);
|
|
1571
|
+
return `${header}\n\n${sections.join("\n\n")}\n`;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* goTypedNativeObserve — the TEST-ONLY observe companion (a SEPARATE file in the SAME package as the
|
|
1575
|
+
* covered module). It carries the typed->Value serializers (ser_*) + an ObserveNativeRaw dispatch
|
|
1576
|
+
* that drives the module-private struct runner and serializes its result to a canonical Value for
|
|
1577
|
+
* the equivalence pin. This lives OUTSIDE the covered module so the module stays literal whole-file
|
|
1578
|
+
* zero (the ser_* NewObj is test glue, never on the consumer's read hot path). Emitted only for the
|
|
1579
|
+
* fully-covered case (the equivalence harness); production consumers never use it.
|
|
1580
|
+
*/
|
|
1581
|
+
export function goTypedNativeObserve(ir, runtimeImport) {
|
|
1582
|
+
const components = ir.components;
|
|
1583
|
+
const native = components.filter(isNativeRaw);
|
|
1584
|
+
const plan = buildTypePlan(ir);
|
|
1585
|
+
const serializers = emitSerializers(plan);
|
|
1586
|
+
const marshallers = goTypedInternals.emitMarshallers(plan);
|
|
1587
|
+
const mustHelpers = emitMustHelpers(plan);
|
|
1588
|
+
// arr/opt node materialize helpers (mustArr_*/mustOpt_*) for the concrete-row decode of a node whose
|
|
1589
|
+
// whole outType is a bare arr/opt (materializeExpr emits mustArr_/mustOpt_ for those).
|
|
1590
|
+
const arrOptHelpers = goTypedInternals.emitArrOptNodeHelpers({ ir }, plan);
|
|
1591
|
+
const arms = native
|
|
1592
|
+
.map((c) => {
|
|
1593
|
+
const outRef = c.outputType !== undefined ? deriveTypeRef(c.outputType, plan) : undefined;
|
|
1594
|
+
const ser = outRef ? serializeTyped("tv", outRef, plan) : "tv";
|
|
1595
|
+
// decode the generic *Obj input into the CONCRETE In_<comp> struct (test glue — ONE decode at the
|
|
1596
|
+
// top, OUTSIDE the covered hot path; a REQUIRED declared input is present so an absent key is a
|
|
1597
|
+
// fail-closed decode error surfaced here, never on the native runner path).
|
|
1598
|
+
return `\tif name == ${JSON.stringify(c.name)} {\n\t\tin, ierr := decode_${inStructName(c.name)}(input)\n\t\tif ierr != nil {\n\t\t\treturn nil, ierr\n\t\t}\n\t\ttv, err := run_native_raw_struct_${sanitize(c.name)}(handlers, in)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn ${ser}, nil\n\t}`;
|
|
1599
|
+
})
|
|
1600
|
+
.join("\n");
|
|
1601
|
+
// input decoders (TEST glue): generic *Obj -> concrete In_<comp>. Scalar fields read the typed value
|
|
1602
|
+
// (a `.(T)` here is on the SDK/wire Value in the OBSERVE companion, never in the covered module).
|
|
1603
|
+
const inDecoders = native.map((c) => emitInDecoder(c)).join("\n\n");
|
|
1604
|
+
// The observe companion drives EVERY covered component, so its handler type param H must satisfy
|
|
1605
|
+
// EVERY per-component concrete Handler_<comp> interface — a combined constraint embeds them all.
|
|
1606
|
+
const combinedConstraint = `allNativeRawHandlers`;
|
|
1607
|
+
const constraintDecl = `// ${combinedConstraint} — the combined constraint embedding every covered component's concrete
|
|
1608
|
+
// handler interface, so one handler value drives all covered runners (test glue only).
|
|
1609
|
+
type ${combinedConstraint} interface {
|
|
1610
|
+
${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
1611
|
+
}`;
|
|
1612
|
+
// ── concrete scripted adapter (TEST glue): implements every Node_* method by draining a per-
|
|
1613
|
+
// COMPONENT scripted-outcome queue and DECODING the scripted ok Value into the CONCRETE row struct.
|
|
1614
|
+
// This is where the generic scripted source (a Value queue) meets the concrete per-node seam: the
|
|
1615
|
+
// decode is generated (it knows the concrete row type), so the runner stays fully concrete. ──
|
|
1616
|
+
const decodeFns = [];
|
|
1617
|
+
const methodImpls = [];
|
|
1618
|
+
const emittedDecode = new Set();
|
|
1619
|
+
for (const c of native) {
|
|
1620
|
+
const typedNodes = new Map();
|
|
1621
|
+
for (const n of c.body)
|
|
1622
|
+
typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
|
|
1623
|
+
for (const n of c.body) {
|
|
1624
|
+
const ref = typedNodes.get(n.id);
|
|
1625
|
+
if ("map" in n) {
|
|
1626
|
+
const m = n.map;
|
|
1627
|
+
const batched = m.batched === true;
|
|
1628
|
+
const arrRef = ref;
|
|
1629
|
+
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
1630
|
+
const elemT = rawElemStructName(c.name, n.id);
|
|
1631
|
+
const batchT = rawRowStructName(c.name, n.id);
|
|
1632
|
+
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
1633
|
+
const portsT = batched ? `${portsStructName(c.name, n.id)}_batch` : portsStructName(c.name, n.id);
|
|
1634
|
+
const retT = batched ? batchT : elemT;
|
|
1635
|
+
const mapBoundT = boundGoType(n, c, typedNodes, plan);
|
|
1636
|
+
if (batched) {
|
|
1637
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${batchT}, bool) {
|
|
1638
|
+
src, ok := s.next(${JSON.stringify(m.component)})
|
|
1639
|
+
if !ok {
|
|
1640
|
+
return ${batchT}{}, false
|
|
1641
|
+
}
|
|
1642
|
+
if src.isErr {
|
|
1643
|
+
return ${batchT}{IsError: true, Err: src.err}, true
|
|
1644
|
+
}
|
|
1645
|
+
arr, _ := src.ok.([]${PKG}.Value)
|
|
1646
|
+
rows := make([]${elemT}, 0, len(arr))
|
|
1647
|
+
for _, ev := range arr {
|
|
1648
|
+
rows = append(rows, ${elemDecode}(ev))
|
|
1649
|
+
}
|
|
1650
|
+
return ${batchT}{Rows: rows}, true
|
|
1651
|
+
}`);
|
|
1652
|
+
}
|
|
1653
|
+
else {
|
|
1654
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${elemT}, bool) {
|
|
1655
|
+
src, ok := s.next(${JSON.stringify(m.component)})
|
|
1656
|
+
if !ok {
|
|
1657
|
+
return ${elemT}{}, false
|
|
1658
|
+
}
|
|
1659
|
+
if src.isErr {
|
|
1660
|
+
return ${elemT}{IsError: true, Err: src.err}, true
|
|
1661
|
+
}
|
|
1662
|
+
return ${elemDecode}(src.ok), true
|
|
1663
|
+
}`);
|
|
1664
|
+
}
|
|
1665
|
+
void retT;
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
const node = n;
|
|
1669
|
+
const rowT = rawRowStructName(c.name, node.id);
|
|
1670
|
+
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
1671
|
+
const nodeBoundT = boundGoType(node, c, typedNodes, plan);
|
|
1672
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, node.id)}(_ ${portsStructName(c.name, node.id)}, _ ${nodeBoundT}) (${rowT}, bool) {
|
|
1673
|
+
src, ok := s.next(${JSON.stringify(node.component)})
|
|
1674
|
+
if !ok {
|
|
1675
|
+
return ${rowT}{}, false
|
|
1676
|
+
}
|
|
1677
|
+
if src.isErr {
|
|
1678
|
+
return ${rowT}{IsError: true, Err: src.err}, true
|
|
1679
|
+
}
|
|
1680
|
+
return ${decode}(src.ok), true
|
|
1681
|
+
}`);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
const adapter = `// scriptSrc — a scripted per-component outcome (an ok Value or an error), drained in order.
|
|
1685
|
+
type scriptSrc struct {
|
|
1686
|
+
isErr bool
|
|
1687
|
+
err string
|
|
1688
|
+
ok ${PKG}.Value
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// ScriptedNativeRaw — the CONCRETE test handler: it satisfies every Handler_<comp> interface by
|
|
1692
|
+
// draining a per-COMPONENT scripted queue and decoding the ok Value into the concrete row struct.
|
|
1693
|
+
// The decode (decodeRow_*) is generated, so the covered runner drives fully-concrete Node_* methods.
|
|
1694
|
+
// EXPORTED so the runner glue (a separate package) can name it for the generic instantiation.
|
|
1695
|
+
type ScriptedNativeRaw struct {
|
|
1696
|
+
mu sync.Mutex
|
|
1697
|
+
queues map[string][]scriptSrc
|
|
1698
|
+
seen int
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// NewScriptedNativeRaw builds the concrete test handler from the JSON handlers spec (the SAME shape
|
|
1702
|
+
// the reference scriptedHandlers reads): { "<component>": {ok|error} | [ {ok|error}, … ] }. Each
|
|
1703
|
+
// component's ok Value is DecodeValue'd once here (test glue); the per-node method decodes it into the
|
|
1704
|
+
// concrete row. Returns the handler + any decode error (surfaced by the runner as a fatal).
|
|
1705
|
+
func NewScriptedNativeRaw(spec *${PKG}.JObj) (*ScriptedNativeRaw, error) {
|
|
1706
|
+
queues := map[string][]scriptSrc{}
|
|
1707
|
+
if spec != nil {
|
|
1708
|
+
for _, comp := range spec.Keys {
|
|
1709
|
+
var entries []${PKG}.JNode
|
|
1710
|
+
switch t := spec.Vals[comp].(type) {
|
|
1711
|
+
case []${PKG}.JNode:
|
|
1712
|
+
entries = t
|
|
1713
|
+
default:
|
|
1714
|
+
entries = []${PKG}.JNode{t}
|
|
1715
|
+
}
|
|
1716
|
+
for _, e := range entries {
|
|
1717
|
+
eo, _ := e.(*${PKG}.JObj)
|
|
1718
|
+
if okNode, has := eo.Get("ok"); has {
|
|
1719
|
+
val, derr := ${PKG}.DecodeValue(okNode)
|
|
1720
|
+
if derr != nil {
|
|
1721
|
+
return nil, derr
|
|
1722
|
+
}
|
|
1723
|
+
queues[comp] = append(queues[comp], scriptSrc{ok: val})
|
|
1724
|
+
continue
|
|
1725
|
+
}
|
|
1726
|
+
ev, _ := eo.Get("error")
|
|
1727
|
+
es, _ := ev.(string)
|
|
1728
|
+
queues[comp] = append(queues[comp], scriptSrc{isErr: true, err: es})
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
return &ScriptedNativeRaw{queues: queues}, nil
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// next drains one scripted outcome for a component (last entry repeats — mirrors the reference
|
|
1736
|
+
// scriptedHandlers), returns ok=false when the component has no scripted queue (fail-closed). The
|
|
1737
|
+
// bc#87 parallel-stage runner calls Node_* from multiple goroutines, so the drain is mutex-guarded.
|
|
1738
|
+
func (s *ScriptedNativeRaw) next(component string) (scriptSrc, bool) {
|
|
1739
|
+
s.mu.Lock()
|
|
1740
|
+
defer s.mu.Unlock()
|
|
1741
|
+
s.seen++
|
|
1742
|
+
q, ok := s.queues[component]
|
|
1743
|
+
if !ok || len(q) == 0 {
|
|
1744
|
+
return scriptSrc{}, false
|
|
1745
|
+
}
|
|
1746
|
+
if len(q) > 1 {
|
|
1747
|
+
v := q[0]
|
|
1748
|
+
s.queues[component] = q[1:]
|
|
1749
|
+
return v, true
|
|
1750
|
+
}
|
|
1751
|
+
return q[0], true
|
|
1752
|
+
}`;
|
|
1753
|
+
return `// GENERATED test-only observe companion (bc#77) — NOT part of the covered module. Serializes the
|
|
1754
|
+
// struct-native runner's result to a canonical Value for the equivalence pin, and carries the CONCRETE
|
|
1755
|
+
// scripted test handler (scriptedNativeRaw) that satisfies every Handler_<comp> interface by decoding a
|
|
1756
|
+
// scripted Value into the concrete per-node row. The consumer never uses this (it keeps the model
|
|
1757
|
+
// native + implements Handler_<comp> over its own wire payload); it exists only so the test harness can
|
|
1758
|
+
// byte-compare the STRUCT-only covered read against run_behavior. Same package as the covered module.
|
|
1759
|
+
package behaviors
|
|
1760
|
+
|
|
1761
|
+
import (
|
|
1762
|
+
"sync"
|
|
1763
|
+
|
|
1764
|
+
${PKG} "${runtimeImport ?? "github.com/foo-ogawa/behavior-contracts/go"}"
|
|
1765
|
+
)
|
|
1766
|
+
|
|
1767
|
+
// typed -> Value serializers (TEST glue only — the covered module has none).
|
|
1768
|
+
${serializers}
|
|
1769
|
+
|
|
1770
|
+
// Value -> typed struct marshallers (+ must* helpers) for the concrete-row decode (TEST glue only).
|
|
1771
|
+
${marshallers}
|
|
1772
|
+
|
|
1773
|
+
${mustHelpers}
|
|
1774
|
+
|
|
1775
|
+
${arrOptHelpers}
|
|
1776
|
+
|
|
1777
|
+
// per-node concrete-row decoders (scripted Value -> RawRow_/RawElem_ struct).
|
|
1778
|
+
${decodeFns.join("\n\n")}
|
|
1779
|
+
|
|
1780
|
+
// input decoders (generic *Obj -> concrete In_<comp>; TEST glue, one decode at the observe boundary).
|
|
1781
|
+
${inDecoders}
|
|
1782
|
+
|
|
1783
|
+
${adapter}
|
|
1784
|
+
|
|
1785
|
+
${constraintDecl}
|
|
1786
|
+
|
|
1787
|
+
// scriptedNativeRaw method impls (one per covered node — the concrete Node_* seam, test glue).
|
|
1788
|
+
${methodImpls.join("\n\n")}
|
|
1789
|
+
|
|
1790
|
+
// ObserveNativeRaw drives the struct-native runner and serializes to a canonical Value (test only).
|
|
1791
|
+
// GENERIC over the CONCRETE combined handler type H — so each enclosed run_native_raw_struct_* call is
|
|
1792
|
+
// instantiated at the concrete handler type (direct, devirtualized per-node method calls), not at an
|
|
1793
|
+
// interface. Instantiate with your concrete handler type to preserve the covered path's concreteness.
|
|
1794
|
+
func ObserveNativeRaw[H ${combinedConstraint}](name string, handlers H, input *${PKG}.Obj) (${PKG}.Value, error) {
|
|
1795
|
+
${arms}
|
|
1796
|
+
return nil, ${PKG}.NewBehaviorFailure("UNKNOWN_ENTRY", "component '"+name+"' is not a covered read (fail-closed)")
|
|
1797
|
+
}
|
|
1798
|
+
`;
|
|
1799
|
+
}
|
|
1800
|
+
/** emitDecodeRow — emit (once, deduped) a decoder `decodeRow_<comp>_<node><suffix>(v Value) <rowT>`
|
|
1801
|
+
* that materializes the scripted ok Value into the concrete row struct (typed fields = the outType /
|
|
1802
|
+
* elem-row type). Named: reuse the Value->struct marshaller + copy fields; scalar/arr/opt: materialize
|
|
1803
|
+
* into .Val. TEST glue only (the production consumer decodes its own wire payload). */
|
|
1804
|
+
function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix) {
|
|
1805
|
+
const fn = `decodeRow_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}${suffix}`;
|
|
1806
|
+
if (emitted.has(fn))
|
|
1807
|
+
return fn;
|
|
1808
|
+
emitted.add(fn);
|
|
1809
|
+
const lines = [];
|
|
1810
|
+
lines.push(`func ${fn}(v ${PKG}.Value) ${rowT} {`);
|
|
1811
|
+
lines.push(`\tvar out ${rowT}`);
|
|
1812
|
+
if (ref.kind === "named") {
|
|
1813
|
+
const decl = findDecl(plan, ref.name);
|
|
1814
|
+
lines.push(`\tt := ${goTypedInternals.materializeExpr("v", ref, plan)}`);
|
|
1815
|
+
for (const f of decl.fields) {
|
|
1816
|
+
lines.push(`\tout.${goFieldName(f.name)} = t.${goFieldName(f.name)}`);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
else {
|
|
1820
|
+
lines.push(`\tout.Val = ${goTypedInternals.materializeExpr("v", ref, plan)}`);
|
|
1821
|
+
}
|
|
1822
|
+
lines.push(`\treturn out`);
|
|
1823
|
+
lines.push(`}`);
|
|
1824
|
+
out.push(lines.join("\n"));
|
|
1825
|
+
return fn;
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* goTypedNativeEmitter — language id `go-typed-native` (bc#77). Emits the generic straight-line
|
|
1829
|
+
* body (for non-covered components / helpers) plus the COMBINED READ layer (native ports struct
|
|
1830
|
+
* IN + raw struct result OUT + inline sequential exec) and the BindNativeRaw surface. Requires
|
|
1831
|
+
* outType/outputType on every covered read (fail-closed otherwise).
|
|
1832
|
+
*/
|
|
1833
|
+
export const goTypedNativeEmitter = {
|
|
1834
|
+
language: "go-typed-native",
|
|
1835
|
+
fileExtension: ".go",
|
|
1836
|
+
defaultRuntimeImport: "github.com/foo-ogawa/behavior-contracts/go/coderuntime",
|
|
1837
|
+
filenameHint: "behaviors.typed.native.generated.go",
|
|
1838
|
+
emit,
|
|
1839
|
+
};
|
|
1840
|
+
//# sourceMappingURL=emit-typed-native-go.js.map
|