behavior-contracts 0.8.4 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring.d.ts.map +1 -1
- package/dist/authoring.js +25 -1
- package/dist/authoring.js.map +1 -1
- package/dist/behavior.d.ts +23 -3
- package/dist/behavior.d.ts.map +1 -1
- package/dist/behavior.js +209 -11
- package/dist/behavior.js.map +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +12 -1
- package/dist/builder.js.map +1 -1
- package/dist/expr-eval.d.ts +7 -1
- package/dist/expr-eval.d.ts.map +1 -1
- package/dist/expr-eval.js +8 -1
- package/dist/expr-eval.js.map +1 -1
- package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-go.js +1306 -710
- package/dist/generator/emit-typed-native-go.js.map +1 -1
- package/dist/generator/emit-typed-native-rust.d.ts +3 -2
- package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-rust.js +1279 -790
- package/dist/generator/emit-typed-native-rust.js.map +1 -1
- package/dist/generator/native-expr.d.ts +23 -2
- package/dist/generator/native-expr.d.ts.map +1 -1
- package/dist/generator/native-expr.js +31 -2
- package/dist/generator/native-expr.js.map +1 -1
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +12 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/plan.d.ts +43 -2
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +19 -5
- package/dist/plan.js.map +1 -1
- package/package.json +2 -2
|
@@ -15,14 +15,34 @@ function goErr(code, msgExpr) {
|
|
|
15
15
|
return `&BehaviorError{Code: ${JSON.stringify(code)}, Message: ${msgExpr}}`;
|
|
16
16
|
}
|
|
17
17
|
/** emitBehaviorErrorType — the LOCAL concrete error type baked into the covered module so a runner can
|
|
18
|
-
* fail closed with ZERO bc-runtime import. `*BehaviorError` satisfies `error
|
|
18
|
+
* fail closed with ZERO bc-runtime import. `*BehaviorError` satisfies `error` and carries the leaf's
|
|
19
|
+
* structured Error Value (scp-error.md "The Error Value"). */
|
|
19
20
|
function emitBehaviorErrorType() {
|
|
20
|
-
return `//
|
|
21
|
+
return `// ErrorDetail — the structured, recoverable payload a failure carries (scp-error.md "The Error
|
|
22
|
+
// Value"). The LEAF produces it at the wire boundary (the only party holding both the declared type
|
|
23
|
+
// and the raw wire datum); the runner transports it verbatim. Concrete fields — no boxed Value, no
|
|
24
|
+
// serialized blob in a string: ExpectedType is Portable Type Notation, a rendering of a STATICALLY
|
|
25
|
+
// DECLARED type, so nothing walks a type at runtime. Kind is the closed set (typeMismatch /
|
|
26
|
+
// missingField / overflow), empty when the failure is not about a datum.
|
|
27
|
+
type ErrorDetail struct {
|
|
28
|
+
Kind string
|
|
29
|
+
Model string
|
|
30
|
+
Field string
|
|
31
|
+
ExpectedType string
|
|
32
|
+
ActualWireType string
|
|
33
|
+
RawValue string
|
|
34
|
+
Context map[string]string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
|
|
21
38
|
// covered runner returns *BehaviorError (which satisfies error) instead of a bc-runtime failure, so
|
|
22
39
|
// the fully-covered module imports ZERO bc runtime. Codes match run_behavior verbatim (byte-equal).
|
|
40
|
+
// Detail carries the leaf's structured Error Value across the seam so a caller can log / skip /
|
|
41
|
+
// repair / migrate rather than parse a message (nil when the failure is not about a datum).
|
|
23
42
|
type BehaviorError struct {
|
|
24
43
|
Code string
|
|
25
44
|
Message string
|
|
45
|
+
Detail *ErrorDetail
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
func (e *BehaviorError) Error() string { return e.Code + ": " + e.Message }
|
|
@@ -30,7 +50,48 @@ func (e *BehaviorError) Error() string { return e.Code + ": " + e.Message }
|
|
|
30
50
|
// FailureCode exposes the stable failure code (byte-equal to run_behavior) WITHOUT a bc-runtime type:
|
|
31
51
|
// a caller that wants the code type-asserts the small local interface { FailureCode() string } rather
|
|
32
52
|
// than a bc failure type — keeping this module runtime-free while a harness can still read the code.
|
|
33
|
-
func (e *BehaviorError) FailureCode() string { return e.Code }
|
|
53
|
+
func (e *BehaviorError) FailureCode() string { return e.Code }
|
|
54
|
+
|
|
55
|
+
// unknownComponent — the fail-closed failure for a catalog name with no handler (byte-equal to
|
|
56
|
+
// run_behavior). In the STATIC codegen model a missing impl is normally a compile error; this covers
|
|
57
|
+
// the scripted test glue's dynamic queue miss.
|
|
58
|
+
func unknownComponent(component string) *BehaviorError {
|
|
59
|
+
return &BehaviorError{Code: "UNKNOWN_COMPONENT", Message: "component '" + component + "' has no handler (fail-closed)"}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// leafFailure — a failure the LEAF reports (test glue): the runner assigns the node's failure code
|
|
63
|
+
// from its Error Policy Kind, so what a leaf carries is its message and its structured Error Value.
|
|
64
|
+
func leafFailure(message string) *BehaviorError {
|
|
65
|
+
return &BehaviorError{Code: "LEAF_FAILURE", Message: message}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// opFailed wraps a leaf failure as the node's Failure under its Error Policy Kind, carrying the
|
|
69
|
+
// leaf's structured detail through unchanged (the runner transports the Error Value, it does not
|
|
70
|
+
// synthesize or re-interpret it).
|
|
71
|
+
func opFailed(node string, policy string, e error) *BehaviorError {
|
|
72
|
+
if be, ok := e.(*BehaviorError); ok {
|
|
73
|
+
return &BehaviorError{Code: "OP_FAILED", Message: "operation '" + node + "' failed under '" + policy + "' policy: " + be.Message, Detail: be.Detail}
|
|
74
|
+
}
|
|
75
|
+
return &BehaviorError{Code: "OP_FAILED", Message: "operation '" + node + "' failed under '" + policy + "' policy: " + e.Error()}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// deTypeMismatch / deMissingField / deOverflow — the de-box mismatch failures the generated decode
|
|
79
|
+
// produces (scp-error.md "The Error Value"). Codes are byte-equal to the runtime outType check
|
|
80
|
+
// (TYPE_MISMATCH / MISSING_PROP) so the covered read stays equivalent to run_behavior; detail.kind
|
|
81
|
+
// distinguishes typeMismatch / missingField / overflow. ErrorDetail is built by a POSITIONAL literal so
|
|
82
|
+
// the offending value is carried without naming the field (the covered module stays whole-file free of
|
|
83
|
+
// the boxed result-type token; its only permitted occurrence is the ErrorDetail field declaration).
|
|
84
|
+
func deTypeMismatch(model, field, expected, actualWire, raw string) *BehaviorError {
|
|
85
|
+
return &BehaviorError{Code: "TYPE_MISMATCH", Message: "node '" + model + "': " + field + ": expected " + expected + ", got " + actualWire, Detail: &ErrorDetail{"typeMismatch", model, field, expected, actualWire, raw, nil}}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func deMissingField(model, field, expected string) *BehaviorError {
|
|
89
|
+
return &BehaviorError{Code: "MISSING_PROP", Message: "node '" + model + "': " + field + ": required field is absent", Detail: &ErrorDetail{"missingField", model, field, expected, "", "", nil}}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func deOverflow(model, field, expected, actualWire, raw string) *BehaviorError {
|
|
93
|
+
return &BehaviorError{Code: "TYPE_MISMATCH", Message: "node '" + model + "': " + field + ": " + raw + " is outside the range of " + expected, Detail: &ErrorDetail{"overflow", model, field, expected, actualWire, raw, nil}}
|
|
94
|
+
}`;
|
|
34
95
|
}
|
|
35
96
|
const { sanitize, emitExpr } = goStraightlineInternals;
|
|
36
97
|
const { goFieldName, renderTypeRef, goScalar, serializeTyped, emitTypeDecls, emitSerializers, emitTypedValue, emitMustHelpers, } = goTypedInternals;
|
|
@@ -39,6 +100,510 @@ const { goFieldName, renderTypeRef, goScalar, serializeTyped, emitTypeDecls, emi
|
|
|
39
100
|
function typedLocal(nodeId) {
|
|
40
101
|
return `t_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
41
102
|
}
|
|
103
|
+
function recordTop(ref) {
|
|
104
|
+
if (ref.kind === "named")
|
|
105
|
+
return { kind: "named", innerName: ref.name };
|
|
106
|
+
if (ref.kind === "arr" && ref.elem.kind === "named")
|
|
107
|
+
return { kind: "arr", innerName: ref.elem.name };
|
|
108
|
+
if (ref.kind === "opt" && ref.inner.kind === "named")
|
|
109
|
+
return { kind: "opt", innerName: ref.inner.name };
|
|
110
|
+
if (ref.kind === "map" && ref.value.kind === "named")
|
|
111
|
+
return { kind: "map", innerName: ref.value.name };
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
/** deBoxEligible — a componentRef node whose result is a record-containing top (named / arr(named) /
|
|
115
|
+
* opt(named) / map(named)). Its handler returns the wire; the generated decode probes each record. */
|
|
116
|
+
function deBoxEligible(ref) {
|
|
117
|
+
return recordTop(ref) !== null;
|
|
118
|
+
}
|
|
119
|
+
/** goWireTypeForTop — the Go handler return type for a record-containing top's wire. */
|
|
120
|
+
function goWireTypeForTop(top) {
|
|
121
|
+
switch (top.kind) {
|
|
122
|
+
case "named":
|
|
123
|
+
return "WireRow";
|
|
124
|
+
case "arr":
|
|
125
|
+
return "[]WireRow";
|
|
126
|
+
case "opt":
|
|
127
|
+
return "WireRow"; // a nil WireRow = None (opt admits absent)
|
|
128
|
+
case "map":
|
|
129
|
+
return "map[string]WireRow";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** notationOfRef — the Portable Type Notation of a TypeRef (scp-error.md), resolving a named ref to its
|
|
133
|
+
* obj notation via the plan. Baked as the `expectedType` literal: a rendering of the STATICALLY declared
|
|
134
|
+
* type, so nothing walks a type at runtime. */
|
|
135
|
+
function notationOfRef(ref, plan) {
|
|
136
|
+
switch (ref.kind) {
|
|
137
|
+
case "scalar":
|
|
138
|
+
return ref.scalar;
|
|
139
|
+
case "opt":
|
|
140
|
+
return `opt(${notationOfRef(ref.inner, plan)})`;
|
|
141
|
+
case "arr":
|
|
142
|
+
return `arr(${notationOfRef(ref.elem, plan)})`;
|
|
143
|
+
case "map":
|
|
144
|
+
return `map(${notationOfRef(ref.value, plan)})`;
|
|
145
|
+
case "named": {
|
|
146
|
+
const decl = findDecl(plan, ref.name);
|
|
147
|
+
return `obj{${decl.fields.map((f) => `${f.name}:${notationOfRef(f.type, plan)}`).join(",")}}`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** emitProbeWireTypes — the once-per-module probe structs + WireRow/WireList seam interfaces. A
|
|
152
|
+
* NumberProbe's Got is the raw numeric text (the de-box parses + range-checks it, so overflow is BC's
|
|
153
|
+
* to detect); every probe carries ActualWireType even on a match so overflow can report the numeric
|
|
154
|
+
* wire tag. Raw is the offending value stringified (NOT named RawValue — that token is reserved for the
|
|
155
|
+
* ErrorDetail field decl). Concrete structs / named interfaces — no boxed Value on the seam. */
|
|
156
|
+
function emitProbeWireTypes() {
|
|
157
|
+
return `// Probe kinds — the outcome of classifying one wire attribute against a declared field type.
|
|
158
|
+
const (
|
|
159
|
+
probeGot uint8 = 0 // present; the wire variant matches the declared type
|
|
160
|
+
probeWrong uint8 = 1 // present but a different wire variant (carries ActualWireType + Raw)
|
|
161
|
+
probeAbsent uint8 = 2 // no attribute for this field
|
|
162
|
+
probeNull uint8 = 3 // present as the producer's null variant (opt → None; required → mismatch)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
// StringProbe / NumberProbe / BoolProbe — a scalar probe result. Got holds the matched value (a
|
|
166
|
+
// NumberProbe's Got is the raw numeric text; the de-box parses + range-checks it). ActualWireType is the
|
|
167
|
+
// producer's own wire tag (a free string, e.g. a DynamoDB "S"/"N"/"BOOL"); Raw is the offending value
|
|
168
|
+
// stringified. Concrete structs — no boxed Value crosses the seam.
|
|
169
|
+
type StringProbe struct {
|
|
170
|
+
Kind uint8
|
|
171
|
+
Got string
|
|
172
|
+
ActualWireType string
|
|
173
|
+
Raw string
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
type NumberProbe struct {
|
|
177
|
+
Kind uint8
|
|
178
|
+
Got string
|
|
179
|
+
ActualWireType string
|
|
180
|
+
Raw string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
type BoolProbe struct {
|
|
184
|
+
Kind uint8
|
|
185
|
+
Got bool
|
|
186
|
+
ActualWireType string
|
|
187
|
+
Raw string
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// RowProbe / ListProbe — a composite probe result: a nested WireRow (a DynamoDB "M") or WireList (an "L").
|
|
191
|
+
type RowProbe struct {
|
|
192
|
+
Kind uint8
|
|
193
|
+
Got WireRow
|
|
194
|
+
ActualWireType string
|
|
195
|
+
Raw string
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
type ListProbe struct {
|
|
199
|
+
Kind uint8
|
|
200
|
+
Got WireList
|
|
201
|
+
ActualWireType string
|
|
202
|
+
Raw string
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// WireRow — the consumer's wire item, probed per declared field by the generated de-box. The consumer
|
|
206
|
+
// implements it over its wire payload (a DynamoDB AttributeValue map) and classifies each attribute's
|
|
207
|
+
// variant; the generated de-box owns strictness. No boxed Value, no dynamic accessor.
|
|
208
|
+
type WireRow interface {
|
|
209
|
+
// Keys reports the attribute keys present (the entries of a wire map / DynamoDB "M"); the de-box
|
|
210
|
+
// iterates them to decode a declared map(V) field.
|
|
211
|
+
Keys() []string
|
|
212
|
+
ProbeString(field string) StringProbe
|
|
213
|
+
ProbeNumber(field string) NumberProbe
|
|
214
|
+
ProbeBool(field string) BoolProbe
|
|
215
|
+
ProbeRow(field string) RowProbe
|
|
216
|
+
ProbeList(field string) ListProbe
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// WireList — a wire array (a DynamoDB "L"), probed per element by the generated de-box.
|
|
220
|
+
type WireList interface {
|
|
221
|
+
Len() int
|
|
222
|
+
ElemString(i int) StringProbe
|
|
223
|
+
ElemNumber(i int) NumberProbe
|
|
224
|
+
ElemBool(i int) BoolProbe
|
|
225
|
+
ElemRow(i int) RowProbe
|
|
226
|
+
ElemList(i int) ListProbe
|
|
227
|
+
}`;
|
|
228
|
+
}
|
|
229
|
+
/** goDecodeFnName — the decode function name for a named type (deduped across the module). */
|
|
230
|
+
function goDecodeNamedName(name) {
|
|
231
|
+
return `decodeWire_${name.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* emitDecodeNamed — emit (once, deduped) `decodeWire_<Name>(w WireRow) (<GoType>, error)` for a named
|
|
235
|
+
* type: probe each declared field from the type SSoT, produce the structured error on a mismatch
|
|
236
|
+
* (deTypeMismatch / deMissingField / deOverflow), and recurse into nested named / arr fields. `model`
|
|
237
|
+
* is the declaring type name (scp-error.md `model`). Nested named types are emitted transitively.
|
|
238
|
+
*/
|
|
239
|
+
function emitDecodeNamed(name, plan, emitted, out) {
|
|
240
|
+
const fn = goDecodeNamedName(name);
|
|
241
|
+
if (emitted.has(fn))
|
|
242
|
+
return fn;
|
|
243
|
+
emitted.add(fn);
|
|
244
|
+
const goType = name; // a named TypeRef renders to its own Go type name.
|
|
245
|
+
const decl = findDecl(plan, name);
|
|
246
|
+
const lines = [];
|
|
247
|
+
lines.push(`// ${fn} — the strict de-box for '${name}': probe each declared field, fail closed on a`);
|
|
248
|
+
lines.push(`// wire mismatch with the structured Error Value (byte-equal codes to run_behavior).`);
|
|
249
|
+
lines.push(`func ${fn}(w WireRow) (${goType}, error) {`);
|
|
250
|
+
lines.push(`\tvar out ${goType}`);
|
|
251
|
+
const ctr = { n: 0 };
|
|
252
|
+
for (const f of decl.fields) {
|
|
253
|
+
lines.push(emitDecodeValue(`out.${goFieldName(f.name)}`, f.type, "w", JSON.stringify(f.name), false, name, f.name, `${goType}{}`, plan, emitted, out, 1, ctr));
|
|
254
|
+
}
|
|
255
|
+
lines.push(`\treturn out, nil`);
|
|
256
|
+
lines.push(`}`);
|
|
257
|
+
out.push(lines.join("\n"));
|
|
258
|
+
return fn;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* emitDecodeValue — the SINGLE recursive decode over the full TypeRef grammar (scalar / opt(scalar) /
|
|
262
|
+
* named / arr / map). It decodes one value of `ref`, obtained by probing `wireVar` at `keyExpr`, and
|
|
263
|
+
* assigns it into the lvalue `target`. Every nested position (an arr element, a map value, an obj field)
|
|
264
|
+
* recurses through THIS function, so arbitrary nesting (map-of-map, arr-of-map, …) works by construction
|
|
265
|
+
* — there is no depth-specific decoder to leave a gap. A mismatch produces the structured Error Value
|
|
266
|
+
* (deTypeMismatch / deMissingField / deOverflow) and returns `retZero`; no panic, no boxed value.
|
|
267
|
+
*
|
|
268
|
+
* - `keyExpr` the Go probe key: a quoted literal `"title"` (obj field), a map-key var, or a list index.
|
|
269
|
+
* - `isElem` probe a WireList element (Elem*, no absent — an index always exists) vs a WireRow field
|
|
270
|
+
* / map key (Probe*, absent → missingField).
|
|
271
|
+
* - `fieldRaw` the raw error `field` label (scp-error.md). `ctr` gives every temp a unique suffix.
|
|
272
|
+
*/
|
|
273
|
+
function emitDecodeValue(target, ref, wireVar, keyExpr, isElem, model, fieldRaw, retZero, plan, emitted, out, indent, ctr) {
|
|
274
|
+
const t = "\t".repeat(indent);
|
|
275
|
+
const ml = JSON.stringify(model);
|
|
276
|
+
const fl = JSON.stringify(fieldRaw);
|
|
277
|
+
const notation = notationOfRef(ref, plan);
|
|
278
|
+
const nl = JSON.stringify(notation);
|
|
279
|
+
const id = `_${ctr.n++}`;
|
|
280
|
+
const lines = [];
|
|
281
|
+
const rowProbe = `${wireVar}.${isElem ? "ElemRow" : "ProbeRow"}(${keyExpr})`;
|
|
282
|
+
const listProbe = `${wireVar}.${isElem ? "ElemList" : "ProbeList"}(${keyExpr})`;
|
|
283
|
+
// an absent attribute is a missing REQUIRED field (a WireList index always exists → no absent check).
|
|
284
|
+
const absent = (pv) => isElem ? "" : `${t}\tif ${pv}.Kind == probeAbsent {\n${t}\t\treturn ${retZero}, deMissingField(${ml}, ${fl}, ${nl})\n${t}\t}\n`;
|
|
285
|
+
const mismatch = (pv, ind) => `${ind}return ${retZero}, deTypeMismatch(${ml}, ${fl}, ${nl}, ${pv}.ActualWireType, ${pv}.Raw)`;
|
|
286
|
+
// opt(scalar): Absent / Null → leave the pointer nil (valid, no error); Wrong → mismatch; Got → &value.
|
|
287
|
+
if (ref.kind === "opt" && ref.inner.kind === "scalar" && ref.inner.scalar !== "null") {
|
|
288
|
+
const pv = `p${id}`;
|
|
289
|
+
lines.push(`${t}{`);
|
|
290
|
+
lines.push(`${t}\t${pv} := ${scalarProbeCall(wireVar, keyExpr, ref.inner.scalar, isElem).call}`);
|
|
291
|
+
lines.push(`${t}\tif ${pv}.Kind == probeGot {`);
|
|
292
|
+
lines.push(scalarAssignInto(target, pv, ref.inner.scalar, model, fieldRaw, notation, `${t}\t\t`, retZero, true));
|
|
293
|
+
lines.push(`${t}\t} else if ${pv}.Kind == probeWrong {`);
|
|
294
|
+
lines.push(mismatch(pv, `${t}\t\t`));
|
|
295
|
+
lines.push(`${t}\t}`);
|
|
296
|
+
lines.push(`${t}}`);
|
|
297
|
+
return lines.join("\n");
|
|
298
|
+
}
|
|
299
|
+
if (ref.kind === "scalar" && ref.scalar !== "null") {
|
|
300
|
+
const pv = `p${id}`;
|
|
301
|
+
lines.push(`${t}{`);
|
|
302
|
+
lines.push(`${t}\t${pv} := ${scalarProbeCall(wireVar, keyExpr, ref.scalar, isElem).call}`);
|
|
303
|
+
if (!isElem)
|
|
304
|
+
lines.push(absent(pv).replace(/\n$/, ""));
|
|
305
|
+
lines.push(`${t}\tif ${pv}.Kind != probeGot {`); // Wrong or Null on a required value → mismatch
|
|
306
|
+
lines.push(mismatch(pv, `${t}\t\t`));
|
|
307
|
+
lines.push(`${t}\t}`);
|
|
308
|
+
lines.push(scalarAssignInto(target, pv, ref.scalar, model, fieldRaw, notation, `${t}\t`, retZero, false));
|
|
309
|
+
lines.push(`${t}}`);
|
|
310
|
+
return lines.join("\n");
|
|
311
|
+
}
|
|
312
|
+
if (ref.kind === "named") {
|
|
313
|
+
const dec = emitDecodeNamed(ref.name, plan, emitted, out);
|
|
314
|
+
const rp = `r${id}`;
|
|
315
|
+
lines.push(`${t}{`);
|
|
316
|
+
lines.push(`${t}\t${rp} := ${rowProbe}`);
|
|
317
|
+
if (!isElem)
|
|
318
|
+
lines.push(absent(rp).replace(/\n$/, ""));
|
|
319
|
+
lines.push(`${t}\tif ${rp}.Kind != probeGot {`);
|
|
320
|
+
lines.push(mismatch(rp, `${t}\t\t`));
|
|
321
|
+
lines.push(`${t}\t}`);
|
|
322
|
+
lines.push(`${t}\tv${id}, e${id} := ${dec}(${rp}.Got)`);
|
|
323
|
+
lines.push(`${t}\tif e${id} != nil {`);
|
|
324
|
+
lines.push(`${t}\t\treturn ${retZero}, e${id}`);
|
|
325
|
+
lines.push(`${t}\t}`);
|
|
326
|
+
lines.push(`${t}\t${target} = v${id}`);
|
|
327
|
+
lines.push(`${t}}`);
|
|
328
|
+
return lines.join("\n");
|
|
329
|
+
}
|
|
330
|
+
if (ref.kind === "arr") {
|
|
331
|
+
const lp = `l${id}`;
|
|
332
|
+
const elemGo = renderTypeRef(ref.elem);
|
|
333
|
+
lines.push(`${t}{`);
|
|
334
|
+
lines.push(`${t}\t${lp} := ${listProbe}`);
|
|
335
|
+
if (!isElem)
|
|
336
|
+
lines.push(absent(lp).replace(/\n$/, ""));
|
|
337
|
+
lines.push(`${t}\tif ${lp}.Kind != probeGot {`);
|
|
338
|
+
lines.push(mismatch(lp, `${t}\t\t`));
|
|
339
|
+
lines.push(`${t}\t}`);
|
|
340
|
+
lines.push(`${t}\t${target} = make([]${elemGo}, 0, ${lp}.Got.Len())`);
|
|
341
|
+
lines.push(`${t}\tfor i${id} := 0; i${id} < ${lp}.Got.Len(); i${id}++ {`);
|
|
342
|
+
lines.push(`${t}\t\tvar el${id} ${elemGo}`);
|
|
343
|
+
lines.push(emitDecodeValue(`el${id}`, ref.elem, `${lp}.Got`, `i${id}`, true, model, fieldRaw, retZero, plan, emitted, out, indent + 2, ctr));
|
|
344
|
+
lines.push(`${t}\t\t${target} = append(${target}, el${id})`);
|
|
345
|
+
lines.push(`${t}\t}`);
|
|
346
|
+
lines.push(`${t}}`);
|
|
347
|
+
return lines.join("\n");
|
|
348
|
+
}
|
|
349
|
+
if (ref.kind === "map") {
|
|
350
|
+
// a declared map(V): the wire value is a producer map (a DynamoDB "M"); enumerate its keys and decode
|
|
351
|
+
// each value as V through THIS same recursion (so map-of-map / map-of-arr nest by construction).
|
|
352
|
+
const mp = `m${id}`;
|
|
353
|
+
const valGo = renderTypeRef(ref.value);
|
|
354
|
+
lines.push(`${t}{`);
|
|
355
|
+
lines.push(`${t}\t${mp} := ${rowProbe}`);
|
|
356
|
+
if (!isElem)
|
|
357
|
+
lines.push(absent(mp).replace(/\n$/, ""));
|
|
358
|
+
lines.push(`${t}\tif ${mp}.Kind != probeGot {`);
|
|
359
|
+
lines.push(mismatch(mp, `${t}\t\t`));
|
|
360
|
+
lines.push(`${t}\t}`);
|
|
361
|
+
lines.push(`${t}\t${target} = make(map[string]${valGo})`);
|
|
362
|
+
lines.push(`${t}\tfor _, k${id} := range ${mp}.Got.Keys() {`);
|
|
363
|
+
lines.push(`${t}\t\tvar mv${id} ${valGo}`);
|
|
364
|
+
lines.push(emitDecodeValue(`mv${id}`, ref.value, `${mp}.Got`, `k${id}`, false, model, fieldRaw, retZero, plan, emitted, out, indent + 2, ctr));
|
|
365
|
+
lines.push(`${t}\t\t${target}[k${id}] = mv${id}`);
|
|
366
|
+
lines.push(`${t}\t}`);
|
|
367
|
+
lines.push(`${t}}`);
|
|
368
|
+
return lines.join("\n");
|
|
369
|
+
}
|
|
370
|
+
// fail closed at generation for a shape the grammar does not cover (e.g. opt of a non-scalar).
|
|
371
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go strict de-box: '${fieldRaw}' of '${model}' has type ${JSON.stringify(ref)} which the generated de-box does not decode (supported: scalar / opt(scalar) / named / arr / map).`);
|
|
372
|
+
}
|
|
373
|
+
/** scalarProbeCall — the probe method call for a scalar of the given kind (string→ProbeString/ElemString,
|
|
374
|
+
* int|float→ProbeNumber/ElemNumber, bool→ProbeBool/ElemBool). `keyOrIndex` is a quoted key (row) or an
|
|
375
|
+
* index var (list). */
|
|
376
|
+
function scalarProbeCall(wireVar, keyOrIndex, scalar, isElem) {
|
|
377
|
+
const m = scalar === "string" ? "String" : scalar === "bool" ? "Bool" : "Number";
|
|
378
|
+
const method = (isElem ? "Elem" : "Probe") + m;
|
|
379
|
+
return { call: `${wireVar}.${method}(${keyOrIndex})` };
|
|
380
|
+
}
|
|
381
|
+
/** scalarAssignInto — assign a probeGot scalar into an lvalue `target` (opt → pointer). Number is parsed
|
|
382
|
+
* + range-checked here (BC owns overflow). No panic on parse failure — a structured overflow error. The
|
|
383
|
+
* `n/nErr/f/fErr/v` temps are safe because every scalar decode is wrapped in its own `{ }` block. */
|
|
384
|
+
function scalarAssignInto(fieldExpr, pv, scalar, model, key, notation, t, retZero, asPtr) {
|
|
385
|
+
const ml = JSON.stringify(model);
|
|
386
|
+
const kl = JSON.stringify(key);
|
|
387
|
+
const nl = JSON.stringify(notation);
|
|
388
|
+
if (scalar === "string") {
|
|
389
|
+
return asPtr ? `${t}v := ${pv}.Got\n${t}${fieldExpr} = &v` : `${t}${fieldExpr} = ${pv}.Got`;
|
|
390
|
+
}
|
|
391
|
+
if (scalar === "bool") {
|
|
392
|
+
return asPtr ? `${t}v := ${pv}.Got\n${t}${fieldExpr} = &v` : `${t}${fieldExpr} = ${pv}.Got`;
|
|
393
|
+
}
|
|
394
|
+
if (scalar === "int") {
|
|
395
|
+
const l = [];
|
|
396
|
+
l.push(`${t}n, nErr := strconv.ParseInt(${pv}.Got, 10, 64)`);
|
|
397
|
+
l.push(`${t}if nErr != nil {`);
|
|
398
|
+
l.push(`${t}\treturn ${retZero}, deOverflow(${ml}, ${kl}, ${nl}, ${pv}.ActualWireType, ${pv}.Got)`);
|
|
399
|
+
l.push(`${t}}`);
|
|
400
|
+
l.push(asPtr ? `${t}${fieldExpr} = &n` : `${t}${fieldExpr} = n`);
|
|
401
|
+
return l.join("\n");
|
|
402
|
+
}
|
|
403
|
+
// float — int widens to float; a non-numeric raw is a range/parse failure (overflow kind).
|
|
404
|
+
const l = [];
|
|
405
|
+
l.push(`${t}f, fErr := strconv.ParseFloat(${pv}.Got, 64)`);
|
|
406
|
+
l.push(`${t}if fErr != nil {`);
|
|
407
|
+
l.push(`${t}\treturn ${retZero}, deOverflow(${ml}, ${kl}, ${nl}, ${pv}.ActualWireType, ${pv}.Got)`);
|
|
408
|
+
l.push(`${t}}`);
|
|
409
|
+
l.push(asPtr ? `${t}${fieldExpr} = &f` : `${t}${fieldExpr} = f`);
|
|
410
|
+
return l.join("\n");
|
|
411
|
+
}
|
|
412
|
+
/** goDecodeElemName — the element de-box function name for a map/fanout node (per node). */
|
|
413
|
+
function goDecodeElemName(compName, nodeId) {
|
|
414
|
+
return `decodeWireElem_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
415
|
+
}
|
|
416
|
+
/** mapFanoutElemRef — the element ROW TypeRef a map/fanout node decodes (the per-element record). */
|
|
417
|
+
function mapFanoutElemRef(node, typedNodes, plan) {
|
|
418
|
+
const ref = typedNodes.get(node.id);
|
|
419
|
+
if ("fanout" in node)
|
|
420
|
+
return fanoutItemsElemRef(node, ref, plan);
|
|
421
|
+
return mapElemRowRef(node, ref, plan);
|
|
422
|
+
}
|
|
423
|
+
/** mapFanoutElemDeBoxEligible — a map/fanout node whose element ROW is a named record de-boxes each
|
|
424
|
+
* element through the generated decodeWireElem_* (closing the scan-row silent-default hole). */
|
|
425
|
+
function mapFanoutElemDeBoxEligible(node, typedNodes, plan) {
|
|
426
|
+
return mapFanoutElemRef(node, typedNodes, plan).kind === "named";
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* emitDecodeElemRow — emit (once) `decodeWireElem_<comp>_<node>(w WireRow) (RawElem_<comp>_<node>, error)`
|
|
430
|
+
* for a map/fanout node whose element ROW is a named record: probe each declared field via the SAME
|
|
431
|
+
* recursion as record reads (emitDecodeValue) and build the concrete RawElem struct, failing closed with
|
|
432
|
+
* the structured Error Value on a mismatch. This is the componentRef seam extended to scan/fan-out rows.
|
|
433
|
+
*/
|
|
434
|
+
function emitDecodeElemRow(comp, node, elemRowRef, plan, emitted, out) {
|
|
435
|
+
const nodeId = node.id;
|
|
436
|
+
const fn = goDecodeElemName(comp.name, nodeId);
|
|
437
|
+
if (emitted.has(fn))
|
|
438
|
+
return fn;
|
|
439
|
+
emitted.add(fn);
|
|
440
|
+
const retType = rawElemStructName(comp.name, nodeId);
|
|
441
|
+
const decl = findDecl(plan, elemRowRef.name);
|
|
442
|
+
const lines = [];
|
|
443
|
+
lines.push(`// ${fn} — the strict de-box for a '${nodeId}' element row: probe each declared field, fail`);
|
|
444
|
+
lines.push(`// closed on a wire mismatch with the structured Error Value (byte-equal codes to run_behavior).`);
|
|
445
|
+
lines.push(`func ${fn}(w WireRow) (${retType}, error) {`);
|
|
446
|
+
lines.push(`\tvar out ${retType}`);
|
|
447
|
+
const ctr = { n: 0 };
|
|
448
|
+
for (const f of decl.fields) {
|
|
449
|
+
lines.push(emitDecodeValue(`out.${goFieldName(f.name)}`, f.type, "w", JSON.stringify(f.name), false, elemRowRef.name, f.name, `${retType}{}`, plan, emitted, out, 1, ctr));
|
|
450
|
+
}
|
|
451
|
+
lines.push(`\treturn out, nil`);
|
|
452
|
+
lines.push(`}`);
|
|
453
|
+
out.push(lines.join("\n"));
|
|
454
|
+
return fn;
|
|
455
|
+
}
|
|
456
|
+
/** emitNodeDecodeFns — emit the decode functions for every de-box-eligible node in a component: a
|
|
457
|
+
* componentRef record read (decodeWire_<Name>) OR a map/fanout element row (decodeWireElem_<comp>_<node>).
|
|
458
|
+
* Deduped; nested named fields recurse through the shared decodeWire_<Name>. */
|
|
459
|
+
/** goDecodeResultName — the top-result decode function name for a de-box componentRef node. */
|
|
460
|
+
function goDecodeResultName(compName, nodeId) {
|
|
461
|
+
return `decodeWireResult_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* emitDecodeWireResult — emit (once) `decodeWireResult_<comp>_<node>(wire <WireType>) (<ResultType>,
|
|
465
|
+
* error)` for a componentRef node whose result is a record-containing top: it decodes each record via
|
|
466
|
+
* decodeWire_<InnerName> and assembles the top (a record / []record / *record / map[string]record),
|
|
467
|
+
* failing closed with the structured Error Value on a mismatch. This is the SAME decode recursion at the
|
|
468
|
+
* node-result position — one path covers named / arr(named) / opt(named) / map(named).
|
|
469
|
+
*/
|
|
470
|
+
function emitDecodeWireResult(comp, node, ref, plan, emitted, out) {
|
|
471
|
+
const top = recordTop(ref);
|
|
472
|
+
const fn = goDecodeResultName(comp.name, node.id);
|
|
473
|
+
if (emitted.has(fn))
|
|
474
|
+
return fn;
|
|
475
|
+
emitted.add(fn);
|
|
476
|
+
const innerDecode = emitDecodeNamed(top.innerName, plan, emitted, out);
|
|
477
|
+
const resultType = renderTypeRef(ref);
|
|
478
|
+
const wireType = goWireTypeForTop(top);
|
|
479
|
+
const lines = [];
|
|
480
|
+
lines.push(`// ${fn} — decode the node's record-containing '${top.kind}' result from the wire (each record`);
|
|
481
|
+
lines.push(`// via ${innerDecode}); a de-box mismatch fails closed with the structured Error Value.`);
|
|
482
|
+
lines.push(`func ${fn}(wire ${wireType}) (${resultType}, error) {`);
|
|
483
|
+
switch (top.kind) {
|
|
484
|
+
case "named":
|
|
485
|
+
lines.push(`\treturn ${innerDecode}(wire)`);
|
|
486
|
+
break;
|
|
487
|
+
case "arr":
|
|
488
|
+
lines.push(`\tout := make(${resultType}, 0, len(wire))`);
|
|
489
|
+
lines.push(`\tfor _, w := range wire {`);
|
|
490
|
+
lines.push(`\t\te, err := ${innerDecode}(w)`);
|
|
491
|
+
lines.push(`\t\tif err != nil {`, `\t\t\treturn nil, err`, `\t\t}`);
|
|
492
|
+
lines.push(`\t\tout = append(out, e)`);
|
|
493
|
+
lines.push(`\t}`);
|
|
494
|
+
lines.push(`\treturn out, nil`);
|
|
495
|
+
break;
|
|
496
|
+
case "opt":
|
|
497
|
+
lines.push(`\tif wire == nil {`, `\t\treturn nil, nil`, `\t}`);
|
|
498
|
+
lines.push(`\tr, err := ${innerDecode}(wire)`);
|
|
499
|
+
lines.push(`\tif err != nil {`, `\t\treturn nil, err`, `\t}`);
|
|
500
|
+
lines.push(`\treturn &r, nil`);
|
|
501
|
+
break;
|
|
502
|
+
case "map":
|
|
503
|
+
lines.push(`\tout := make(${resultType})`);
|
|
504
|
+
lines.push(`\tfor k, w := range wire {`);
|
|
505
|
+
lines.push(`\t\tr, err := ${innerDecode}(w)`);
|
|
506
|
+
lines.push(`\t\tif err != nil {`, `\t\t\treturn nil, err`, `\t\t}`);
|
|
507
|
+
lines.push(`\t\tout[k] = r`);
|
|
508
|
+
lines.push(`\t}`);
|
|
509
|
+
lines.push(`\treturn out, nil`);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
lines.push(`}`);
|
|
513
|
+
out.push(lines.join("\n"));
|
|
514
|
+
return fn;
|
|
515
|
+
}
|
|
516
|
+
function emitNodeDecodeFns(comp, typedNodes, plan, emitted, out) {
|
|
517
|
+
for (const n of comp.body) {
|
|
518
|
+
if ("cond" in n)
|
|
519
|
+
continue;
|
|
520
|
+
if ("map" in n || "fanout" in n) {
|
|
521
|
+
const node = n;
|
|
522
|
+
if (!mapFanoutElemDeBoxEligible(node, typedNodes, plan))
|
|
523
|
+
continue;
|
|
524
|
+
emitDecodeElemRow(comp, node, mapFanoutElemRef(node, typedNodes, plan), plan, emitted, out);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const ref = typedNodes.get(n.id);
|
|
528
|
+
if (ref === undefined)
|
|
529
|
+
continue;
|
|
530
|
+
const top = recordTop(ref);
|
|
531
|
+
if (top === null)
|
|
532
|
+
continue;
|
|
533
|
+
// a NAMED top decodes in place via decodeWire_<Name>; an arr/opt/map top assembles via
|
|
534
|
+
// decodeWireResult_* (which itself recurses into decodeWire_<Name> per record).
|
|
535
|
+
if (top.kind === "named")
|
|
536
|
+
emitDecodeNamed(top.innerName, plan, emitted, out);
|
|
537
|
+
else
|
|
538
|
+
emitDecodeWireResult(comp, n, ref, plan, emitted, out);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/** containsNamed — whether a TypeRef transitively contains a named record (through arr / opt / map). */
|
|
542
|
+
function containsNamed(ref) {
|
|
543
|
+
switch (ref.kind) {
|
|
544
|
+
case "scalar":
|
|
545
|
+
return false;
|
|
546
|
+
case "named":
|
|
547
|
+
return true;
|
|
548
|
+
case "opt":
|
|
549
|
+
return containsNamed(ref.inner);
|
|
550
|
+
case "arr":
|
|
551
|
+
return containsNamed(ref.elem);
|
|
552
|
+
case "map":
|
|
553
|
+
return containsNamed(ref.value);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/** readsAPriorNode — a componentRef node is a DATAFLOW consumer iff at least one input port is a ref
|
|
557
|
+
* headed by a PRIOR BODY NODE (it reads a prior node's already-native result, #129), rather than a fresh
|
|
558
|
+
* external wire read whose ports come from input params / static / an element ($as) binding. This is the
|
|
559
|
+
* same head-is-a-body-node signal priorNodeArrElemInfo (#129) uses to lower a prior-node array feed. */
|
|
560
|
+
function readsAPriorNode(node, typedNodes) {
|
|
561
|
+
for (const expr of Object.values(node.ports ?? {})) {
|
|
562
|
+
const e = classifyExpr(expr);
|
|
563
|
+
if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0]))
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* assertDeBoxableResults — fail closed (never a silent trust) for a covered node whose result would
|
|
570
|
+
* materialize on the trusting typed path and silently default a wrong wire value. Two shapes fail closed:
|
|
571
|
+
* (1) a NON-named top (arr / opt / map) that transitively contains a record the de-box does not decode
|
|
572
|
+
* (deeper nesting like arr(arr(named))) — it would trust the record's wire fields; (2) a HANDLER-BACKED
|
|
573
|
+
* componentRef node whose result is a pure scalar / scalar-collection top (arr(scalar) / opt(scalar) /
|
|
574
|
+
* map(scalar) / bare scalar) — it materializes `t = row.Val` on the trusting path, so a wrong wire value
|
|
575
|
+
* silently defaults (diverging from run_behavior's outType check). Either is a loud generation-time error
|
|
576
|
+
* (native-codegen-standard §2 — an unsupported shape fails closed, never escapes to a trusting path). A
|
|
577
|
+
* record top (de-boxed) and a DATAFLOW scalar top (a prior-node-fed result, computed from already-native
|
|
578
|
+
* data — #129 folds) both generate.
|
|
579
|
+
*/
|
|
580
|
+
function assertDeBoxableResults(comp, typedNodes, plan) {
|
|
581
|
+
for (const n of comp.body) {
|
|
582
|
+
if ("cond" in n)
|
|
583
|
+
continue;
|
|
584
|
+
const isMapFanout = "map" in n || "fanout" in n;
|
|
585
|
+
const ref = isMapFanout ? mapFanoutElemRef(n, typedNodes, plan) : typedNodes.get(n.id);
|
|
586
|
+
const deBoxed = isMapFanout ? ref.kind === "named" : recordTop(ref) !== null;
|
|
587
|
+
if (deBoxed)
|
|
588
|
+
continue;
|
|
589
|
+
const nodeId = n.id;
|
|
590
|
+
if (containsNamed(ref)) {
|
|
591
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go strict de-box: node '${nodeId}' shape ${JSON.stringify(ref)} contains a record the de-box does not decode (a deeper nesting than a direct named / arr / opt / map of a named record). It would trust the record's wire fields (silent default) — the silent-default red line forbids. Fail closed: reshape so the record is a direct de-box top, or extend the recursion for this nesting.`);
|
|
592
|
+
}
|
|
593
|
+
// a map/fanout element is ALWAYS a per-element wire read; only a named record is de-boxed
|
|
594
|
+
// (decodeWireElem_*). A scalar / scalar-collection element materializes from the trusting per-element
|
|
595
|
+
// `.Val` — a wrong wire value silent-defaults. Fail closed.
|
|
596
|
+
if (isMapFanout) {
|
|
597
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go strict de-box: map/fanout node '${nodeId}' element ${JSON.stringify(ref)} is a scalar / scalar-collection per-element wire read that materializes on the trusting typed path — a wrong wire value would silent-default (diverging from run_behavior's outType check). Fail closed: declare the element as a record (de-boxed).`);
|
|
598
|
+
}
|
|
599
|
+
// a pure-scalar / scalar-collection top on a HANDLER-BACKED componentRef wire read trusts `t = row.Val`
|
|
600
|
+
// (a wrong wire value silent-defaults). A DATAFLOW consumer (a port fed by a prior node) computes from
|
|
601
|
+
// already-native data and generates.
|
|
602
|
+
if (!readsAPriorNode(n, typedNodes)) {
|
|
603
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go strict de-box: node '${nodeId}' result top ${JSON.stringify(ref)} is a handler-backed scalar / scalar-collection read that materializes on the trusting typed path — a wrong wire value would silent-default (diverging from run_behavior's outType check). Fail closed: declare the result as a record (de-boxed) or feed it as dataflow from a prior node.`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
42
607
|
function goPortFieldName(wire) {
|
|
43
608
|
const safe = wire.replace(/[^A-Za-z0-9_]/g, "_");
|
|
44
609
|
const head = safe.charAt(0);
|
|
@@ -49,61 +614,43 @@ function portsStructName(compName, nodeId) {
|
|
|
49
614
|
// type to implement the per-component Handler_<comp> interface's concrete Node_* method signatures.
|
|
50
615
|
return `PortsNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
51
616
|
}
|
|
52
|
-
function inferPortType(node, inputPorts, asBinding) {
|
|
53
|
-
if (staticArrElems(node) !== null)
|
|
54
|
-
return "Value";
|
|
55
|
-
const e = classifyExpr(node);
|
|
56
|
-
switch (e.kind) {
|
|
57
|
-
case "str":
|
|
58
|
-
case "concat":
|
|
59
|
-
return "string";
|
|
60
|
-
case "bool":
|
|
61
|
-
return "bool";
|
|
62
|
-
case "ref": {
|
|
63
|
-
// #108: a `$as`-headed ref (map element binding) — resolve the element field's scalar type.
|
|
64
|
-
if (asBinding !== undefined && e.path[0] === asBinding.name) {
|
|
65
|
-
try {
|
|
66
|
-
const { ref } = goTypedInternals.typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan);
|
|
67
|
-
if (ref.kind === "scalar" && ref.scalar !== "null")
|
|
68
|
-
return goScalarPortField(ref.scalar);
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
return "Value";
|
|
72
|
-
}
|
|
73
|
-
return "Value";
|
|
74
|
-
}
|
|
75
|
-
// a ref to a REQUIRED scalar input port → that scalar. An OPTIONAL port has no required-scalar
|
|
76
|
-
// kind (its native type is *T) and is lowered by the opt lane ahead of this — reaching here with
|
|
77
|
-
// one means the shape is uncovered, and "Value" routes it to the caller's loud fail-closed.
|
|
78
|
-
if (e.path.length === 1) {
|
|
79
|
-
const sc = inputScalarKind(inputPorts[e.path[0]]);
|
|
80
|
-
if (sc !== undefined)
|
|
81
|
-
return sc;
|
|
82
|
-
}
|
|
83
|
-
return "Value";
|
|
84
|
-
}
|
|
85
|
-
default:
|
|
86
|
-
return "Value";
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
617
|
/** scalar TypeRef → PortFieldType (native go scalar kind). */
|
|
90
618
|
function goScalarPortField(scalar) {
|
|
91
619
|
return scalar === "string" ? "string" : scalar === "int" ? "int64" : scalar === "float" ? "float64" : "bool";
|
|
92
620
|
}
|
|
93
|
-
/**
|
|
94
|
-
*
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
621
|
+
/**
|
|
622
|
+
* goNativeResolveHead — the ONE ref-head resolver the runtime-free native-expr compiler reads, for cond
|
|
623
|
+
* `if`/branches, a map/fanout `when` guard, AND a port. A head is a `$as` element binding, a prior body
|
|
624
|
+
* node's typed local, or an input port.
|
|
625
|
+
*/
|
|
626
|
+
function goNativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
|
|
627
|
+
return (head) => {
|
|
628
|
+
if (opts?.asBinding !== undefined && opts.asBase !== undefined && head === opts.asBinding.name) {
|
|
629
|
+
return { expr: opts.asBase, ref: opts.asBinding.ref };
|
|
630
|
+
}
|
|
631
|
+
if (isPrior(head))
|
|
632
|
+
return { expr: typedLocal(head), ref: typedNodes.get(head) };
|
|
633
|
+
return goInputHeadRef(head, comp, plan);
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* compilePortNode — lower a port expression to its native ports-struct field via the runtime-free
|
|
638
|
+
* native-expr compiler (native-expr.ts): the ONE port lowering. The port's field TYPE is the compiled
|
|
639
|
+
* `ref`, its initializer the compiled `expr` — derived from a SINGLE lowering, so they cannot disagree.
|
|
640
|
+
* Any closed operator, a literal, a ref (input scalar/array/map, prior-node scalar/array, `$as` element
|
|
641
|
+
* field), a concat, a static array, an optional-input read — all resolve here. A port that cannot lower
|
|
642
|
+
* FAILS CLOSED. A FALLIBLE lowering (hoists statements) is returned as-is; only emitPortInit rejects it.
|
|
643
|
+
*/
|
|
644
|
+
function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
|
|
645
|
+
const resolveHead = goNativeResolveHead(comp, plan, typedNodes, isPrior, opts);
|
|
646
|
+
try {
|
|
647
|
+
return new NativeExprCompiler(makeGoExprBackend("", resolveHead, plan)).compilePort(node);
|
|
648
|
+
}
|
|
649
|
+
catch (e) {
|
|
650
|
+
if (!(e instanceof GeneratorFailure))
|
|
651
|
+
throw e;
|
|
652
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} ${JSON.stringify(node)} does not lower to a native Go value: ${e.message}`);
|
|
105
653
|
}
|
|
106
|
-
return out;
|
|
107
654
|
}
|
|
108
655
|
/**
|
|
109
656
|
* portFieldGoType — the NATIVE Go type for a covered port field (bc#90 / runtime-free). The covered
|
|
@@ -115,96 +662,9 @@ function staticStringArrElems(node) {
|
|
|
115
662
|
* Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no
|
|
116
663
|
* `dslcontracts.Value` fallback on the native module). `where` names the emission site for the error.
|
|
117
664
|
*/
|
|
118
|
-
function portFieldGoType(node,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// required-scalar inference below (which has no way to say "absent").
|
|
122
|
-
const optPort = optPortCompileG(node, inputPorts, where, plan);
|
|
123
|
-
if (optPort !== null)
|
|
124
|
-
return renderTypeRef(optPort.ref);
|
|
125
|
-
if (staticStringArrElems(node) !== null)
|
|
126
|
-
return "[]string";
|
|
127
|
-
// #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → []ElemT.
|
|
128
|
-
const arrElem = portArrayElemRef(node, inputPorts, plan);
|
|
129
|
-
if (arrElem !== null)
|
|
130
|
-
return `[]${renderTypeRef(arrElem)}`;
|
|
131
|
-
// #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) → []ElemT.
|
|
132
|
-
const priorArr = priorNodeArrElemInfo(node, typedNodes, plan);
|
|
133
|
-
if (priorArr !== null)
|
|
134
|
-
return `[]${renderTypeRef(priorArr.elemRef)}`;
|
|
135
|
-
const num = numLiteralGoExpr(node);
|
|
136
|
-
if (num !== null)
|
|
137
|
-
return Number.isInteger(node) ? "int64" : "float64";
|
|
138
|
-
// a ref to a `$as` element field (or an input map port) whose type is a MAP or NAMED struct (NOT a bare
|
|
139
|
-
// scalar) → the native map/struct type (#108-map: a `{map:V}` write port lowers to map[string]V).
|
|
140
|
-
const composite = portCompositeRef(node, inputPorts, asBinding, plan);
|
|
141
|
-
if (composite !== null)
|
|
142
|
-
return renderTypeRef(composite);
|
|
143
|
-
const ft = inferPortType(node, inputPorts, asBinding);
|
|
144
|
-
if (ft === "Value") {
|
|
145
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): ${where} '${JSON.stringify(node)}' does not lower to a native Go type (would need a boxed dslcontracts.Value). The covered read plane is runtime-free and admits NO boxed escape. ${GO_STATIC_PORT_FORMS} Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
|
|
146
|
-
}
|
|
147
|
-
return ft;
|
|
148
|
-
}
|
|
149
|
-
/** portCompositeRef (go twin) — the full TypeRef of a port that is a `{ref:[...]}` into a `$as` element
|
|
150
|
-
* MAP or NAMED field (or an input MAP port with elemType). Returns null otherwise (caller falls through
|
|
151
|
-
* to scalar/array inference). #108-map: enables a `{map:V}` write port to lower to map[string]V. */
|
|
152
|
-
function portCompositeRef(node, inputPorts, asBinding, plan) {
|
|
153
|
-
const e = classifyExpr(node);
|
|
154
|
-
if (e.kind !== "ref" || plan === undefined)
|
|
155
|
-
return null;
|
|
156
|
-
let ref = null;
|
|
157
|
-
if (asBinding !== undefined && e.path[0] === asBinding.name) {
|
|
158
|
-
try {
|
|
159
|
-
ref = goTypedInternals.typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan).ref;
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
else if (e.path.length === 1) {
|
|
166
|
-
const s = inputPorts[e.path[0]];
|
|
167
|
-
if (s !== undefined && s.type === "map") {
|
|
168
|
-
const et = s.elemType;
|
|
169
|
-
if (et !== undefined)
|
|
170
|
-
return { kind: "map", value: deriveTypeRef(et, plan) };
|
|
171
|
-
}
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
if (ref === null)
|
|
175
|
-
return null;
|
|
176
|
-
if (ref.kind === "map" || ref.kind === "named")
|
|
177
|
-
return ref;
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
/** emitCompositePortValue (go twin) — the OWNED native value expr for a `{map:V}`/named port ref: a typed
|
|
181
|
-
* field access `<elemBase>.<field>` (native map/struct, ZERO boxed Value). `elemBaseExpr` is the map arm's
|
|
182
|
-
* element base for a `$as`-headed ref (omitted at top level — only input map-port refs resolve there). */
|
|
183
|
-
function emitCompositePortValue(node, inputPorts, asBinding, plan, elemBaseExpr) {
|
|
184
|
-
const e = classifyExpr(node);
|
|
185
|
-
if (e.kind !== "ref")
|
|
186
|
-
return null;
|
|
187
|
-
if (asBinding !== undefined && elemBaseExpr !== undefined && e.path[0] === asBinding.name) {
|
|
188
|
-
let acc;
|
|
189
|
-
try {
|
|
190
|
-
acc = goTypedInternals.typedFieldAccess(elemBaseExpr, asBinding.ref, e.path.slice(1), plan);
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
if (acc.ref.kind !== "map" && acc.ref.kind !== "named")
|
|
196
|
-
return null;
|
|
197
|
-
return acc.expr;
|
|
198
|
-
}
|
|
199
|
-
// a WHOLE-port ref to an input MAP port (with elemType) — valid at top level OR inside a map node (a
|
|
200
|
-
// ref head that is NOT the `$as` element binding). Reads the native map[string]V off the input struct.
|
|
201
|
-
if ((asBinding === undefined || e.path[0] !== asBinding.name) && e.path.length === 1) {
|
|
202
|
-
const s = inputPorts[e.path[0]];
|
|
203
|
-
if (s !== undefined && s.type === "map" && s.elemType !== undefined) {
|
|
204
|
-
return `in.${goFieldName(e.path[0])}`;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return null;
|
|
665
|
+
function portFieldGoType(node, comp, plan, typedNodes, where = "port", opts) {
|
|
666
|
+
const c = compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), where, opts);
|
|
667
|
+
return c.renderedType ?? renderTypeRef(c.ref);
|
|
208
668
|
}
|
|
209
669
|
function staticArrElems(node) {
|
|
210
670
|
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
@@ -215,36 +675,6 @@ function staticArrElems(node) {
|
|
|
215
675
|
const arg = node.arr;
|
|
216
676
|
return Array.isArray(arg) ? arg : null;
|
|
217
677
|
}
|
|
218
|
-
/**
|
|
219
|
-
* portArrayElemRef (#110) — resolve a componentRef input port that binds a RUNTIME ARRAY (an IN-list /
|
|
220
|
-
* array-bound WHERE head) to its native element TypeRef, or null when the port is not a covered array.
|
|
221
|
-
* The covered shape is a SINGLE-SEGMENT, non-opt `ref` into an INPUT ARRAY port that carries a resolvable
|
|
222
|
-
* `elemType` (scp-ir-architecture.md §5.2 / PortSchema.elemType — BC does NOT infer, consumer-interface
|
|
223
|
-
* C3). The port then lowers to a native `[]ElemT` field fed straight from `in.<Field>` — ZERO boxed Value
|
|
224
|
-
* on the read hot path. This REUSES the bc#108 `elemType` element lowering (deriveTypeRef over the input
|
|
225
|
-
* port's elemType), extended from the `map…over` element-source path to a plain componentRef port.
|
|
226
|
-
*
|
|
227
|
-
* A static string array is a DIFFERENT covered shape ([]string, staticStringArrElems) — handled ahead of
|
|
228
|
-
* this. An array port with NO resolvable elemType, an opt/multi-segment ref, or a non-array input schema
|
|
229
|
-
* resolves to null here → the caller keeps its existing fail-closed behaviour (never a silent box).
|
|
230
|
-
* Requires a TypePlan (an obj elemType maps to a named decl); without one it stays null (fail-closed).
|
|
231
|
-
*/
|
|
232
|
-
function portArrayElemRef(node, inputPorts, plan) {
|
|
233
|
-
if (plan === undefined)
|
|
234
|
-
return null;
|
|
235
|
-
if (staticStringArrElems(node) !== null)
|
|
236
|
-
return null; // the static-string-array port ([]string) — not this shape.
|
|
237
|
-
const e = classifyExpr(node);
|
|
238
|
-
if (e.kind !== "ref" || e.opt || e.path.length !== 1)
|
|
239
|
-
return null;
|
|
240
|
-
const schema = inputPorts?.[e.path[0]];
|
|
241
|
-
if (schema === undefined || (schema.type !== "array" && schema.type !== "arr"))
|
|
242
|
-
return null;
|
|
243
|
-
const et = schema.elemType;
|
|
244
|
-
if (et === undefined)
|
|
245
|
-
return null;
|
|
246
|
-
return deriveTypeRef(et, plan);
|
|
247
|
-
}
|
|
248
678
|
/**
|
|
249
679
|
* isNativeRawComponent — a component covered by the combined read emitter. Strictly sequential
|
|
250
680
|
* (no real concurrency), body is ALL componentRef (no map/cond), default 'fail'/'continue'
|
|
@@ -280,31 +710,49 @@ function numLiteralGoExpr(node) {
|
|
|
280
710
|
// fractional / exponent literal → checked float64 (finite, already guaranteed above).
|
|
281
711
|
return `${PKG}.Value(float64(${node}))`;
|
|
282
712
|
}
|
|
283
|
-
/** A port node is static (literal / ref / concat of those / static arr / bare number literal
|
|
284
|
-
*
|
|
285
|
-
|
|
286
|
-
*
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
713
|
+
/** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
|
|
714
|
+
* resolvable to a native Go Value with no NewObj / no evaluate interpreter. */
|
|
715
|
+
/** planForComp — a TypePlan for a single component (its named types are component-scoped, so a
|
|
716
|
+
* one-component IR yields the same plan the full-IR build would for this component). */
|
|
717
|
+
function planForComp(comp) {
|
|
718
|
+
return buildTypePlan({ irVersion: 1, exprVersion: 2, components: [comp] });
|
|
719
|
+
}
|
|
720
|
+
/** buildTypedNodes — the prior-node typed-local TypeRef map for a component (control-gate nodes are
|
|
721
|
+
* typeless; a map node carries its produced-array ref). An untyped node is omitted (not an error here). */
|
|
722
|
+
function buildTypedNodes(comp, plan) {
|
|
723
|
+
const typedNodes = new Map();
|
|
724
|
+
for (const n of comp.body) {
|
|
725
|
+
if (isControlGate(n))
|
|
726
|
+
continue;
|
|
727
|
+
try {
|
|
728
|
+
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
/* untyped node — omit */
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return typedNodes;
|
|
735
|
+
}
|
|
736
|
+
/** mapAsBinding — the `$as` element binding for a map/fanout node's element ports (the over-element
|
|
737
|
+
* TypeRef), or undefined when the over source does not resolve (an uncovered map). */
|
|
738
|
+
function mapAsBinding(comp, node, typedNodes, plan) {
|
|
739
|
+
try {
|
|
740
|
+
return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
return undefined;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
/** portIsStatic — the ELIGIBILITY gate ASKS the one port lowering (compilePortNode / native-expr)
|
|
747
|
+
* whether it can lower this port, rather than re-deriving the shape rules — so the gate and the emitter
|
|
748
|
+
* can never disagree. A port that does not lower (or lowers fallibly) fails closed. */
|
|
749
|
+
function portIsStatic(node, comp, plan, typedNodes, asBinding) {
|
|
750
|
+
try {
|
|
751
|
+
compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), "port", asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined);
|
|
296
752
|
return true;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
case "bool":
|
|
301
|
-
case "null":
|
|
302
|
-
case "ref":
|
|
303
|
-
return true;
|
|
304
|
-
case "concat":
|
|
305
|
-
return e.parts.every((p) => portIsStatic(reconstructG(p), inputPorts));
|
|
306
|
-
default:
|
|
307
|
-
return false;
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
return false;
|
|
308
756
|
}
|
|
309
757
|
}
|
|
310
758
|
/** The output must lower to a typed struct/value with no Value-scope read: a ref whose head is a
|
|
@@ -385,7 +833,7 @@ function coveredMapNode(n, bodyIds, comp) {
|
|
|
385
833
|
// A map node must carry an outType (its ELEMENT type per the recorder SoT — authoring.ts recordMap
|
|
386
834
|
// holds `node.outType = mapElem`, the PRE-array-wrap element; the emitter synthesizes the produced
|
|
387
835
|
// `[]element`). A map WITHOUT outType is uncovered. Legacy hand-built IR that already carries the
|
|
388
|
-
//
|
|
836
|
+
// The map node's outType is the ELEMENT type; mapNodeArrRef wraps it to the produced array ref.
|
|
389
837
|
const outT = n.outType;
|
|
390
838
|
if (outT === undefined || typeof outT !== "object")
|
|
391
839
|
return false;
|
|
@@ -405,8 +853,11 @@ function coveredMapNode(n, bodyIds, comp) {
|
|
|
405
853
|
return false;
|
|
406
854
|
}
|
|
407
855
|
const ports = m.ports;
|
|
856
|
+
const plan = planForComp(comp);
|
|
857
|
+
const typedNodes = buildTypedNodes(comp, plan);
|
|
858
|
+
const asBinding = mapAsBinding(comp, n, typedNodes, plan);
|
|
408
859
|
for (const p of Object.keys(ports))
|
|
409
|
-
if (!portIsStatic(ports[p], comp
|
|
860
|
+
if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
|
|
410
861
|
return false;
|
|
411
862
|
return true;
|
|
412
863
|
}
|
|
@@ -438,8 +889,17 @@ function coveredFanoutNode(n, bodyIds, comp) {
|
|
|
438
889
|
return false;
|
|
439
890
|
}
|
|
440
891
|
const ports = f.ports;
|
|
892
|
+
const plan = planForComp(comp);
|
|
893
|
+
const typedNodes = buildTypedNodes(comp, plan);
|
|
894
|
+
let asBinding;
|
|
895
|
+
try {
|
|
896
|
+
asBinding = { name: f.as, ref: fanoutOverElemInfo(comp, n, typedNodes, plan).elemRef, plan };
|
|
897
|
+
}
|
|
898
|
+
catch {
|
|
899
|
+
asBinding = undefined;
|
|
900
|
+
}
|
|
441
901
|
for (const p of Object.keys(ports))
|
|
442
|
-
if (!portIsStatic(ports[p], comp
|
|
902
|
+
if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
|
|
443
903
|
return false;
|
|
444
904
|
return true;
|
|
445
905
|
}
|
|
@@ -460,6 +920,8 @@ function isSequentialComponentRefRead(comp) {
|
|
|
460
920
|
* user's signal to add native coverage, fix a genuinely-dynamic spec, or use the --ir dynamic surface.
|
|
461
921
|
*/
|
|
462
922
|
function coverageRejectReason(comp) {
|
|
923
|
+
const crPlan = planForComp(comp);
|
|
924
|
+
const crTypedNodes = buildTypedNodes(comp, crPlan);
|
|
463
925
|
const ep = execPlan(comp);
|
|
464
926
|
if (ep === null)
|
|
465
927
|
return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
|
|
@@ -546,7 +1008,7 @@ function coverageRejectReason(comp) {
|
|
|
546
1008
|
if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
547
1009
|
return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
|
|
548
1010
|
for (const p of Object.keys(cr.ports))
|
|
549
|
-
if (portIsStatic(cr.ports[p], comp
|
|
1011
|
+
if (portIsStatic(cr.ports[p], comp, crPlan, crTypedNodes) === false)
|
|
550
1012
|
return `node '${n.id}' port '${p}' is not statically resolvable`;
|
|
551
1013
|
}
|
|
552
1014
|
if (!outputIsNativeLowerable(comp))
|
|
@@ -568,7 +1030,12 @@ function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
|
|
|
568
1030
|
if (portNames.length === 0) {
|
|
569
1031
|
return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
|
|
570
1032
|
}
|
|
571
|
-
|
|
1033
|
+
// a node WITH ports always resolves through the one port lowering, which needs the type plan + the
|
|
1034
|
+
// prior-node map (both are supplied by every caller — narrow the optional params for the fields below).
|
|
1035
|
+
if (plan === undefined || typedNodes === undefined) {
|
|
1036
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native: ports struct for node '${node.id}' needs a type plan + prior-node map`);
|
|
1037
|
+
}
|
|
1038
|
+
const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp, plan, typedNodes, `port '${p}' of node '${node.id}'`, asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined));
|
|
572
1039
|
const fieldNames = portNames.map(goPortFieldName);
|
|
573
1040
|
const fields = portNames
|
|
574
1041
|
.map((p, i) => `\t${fieldNames[i]} ${fieldTypes[i]} // ${JSON.stringify(p)}`)
|
|
@@ -587,7 +1054,7 @@ ${fields}
|
|
|
587
1054
|
//
|
|
588
1055
|
// The covered path no longer crosses a generic `RawValue`/`RawRow` on the RESULT plane. Each
|
|
589
1056
|
// covered node has a CONCRETE row struct `RawRow_<comp>_<node>` whose fields are the node's
|
|
590
|
-
// projected outType fields (typed, flattened)
|
|
1057
|
+
// projected outType fields (typed, flattened). A row models a SUCCESS; failure travels the (T,
|
|
591
1058
|
// per-component `Handler_<comp>` interface has one method per node returning that concrete struct;
|
|
592
1059
|
// the runner reads `row.<Field>` DIRECTLY (no `.(RawRow)`, no `.(scalar)`, no `RawValue`) and copies
|
|
593
1060
|
// each field into the node's outType struct local. The `.(T)` type-assert seam that the generic
|
|
@@ -595,7 +1062,7 @@ ${fields}
|
|
|
595
1062
|
//
|
|
596
1063
|
// A scalar/arr/opt node (outType not a named struct) has a single `Val <typed>` payload field. A
|
|
597
1064
|
// named-struct node has one Go field per outType field (same names/types as the outType decl), so
|
|
598
|
-
// `RawRow_<comp>_<node>` is structurally the outType struct
|
|
1065
|
+
// `RawRow_<comp>_<node>` is structurally the outType struct. The copier is then a
|
|
599
1066
|
// mechanical field-for-field assignment into the outType struct — NO dynamic value read anywhere.
|
|
600
1067
|
/** Row_<comp>_<node> — the concrete per-node handler-result row struct name. (Deliberately NOT
|
|
601
1068
|
* prefixed with the generic dynamic-accessor name — that accessor is what the concrete path removes.) */
|
|
@@ -627,7 +1094,7 @@ function nodeMethodName(compName, nodeId) {
|
|
|
627
1094
|
* emitRawRowStructs — emit the concrete `RawRow_<comp>_<node>` struct per covered node (and, for a
|
|
628
1095
|
* covered map node, the per-element `RawElem_<comp>_<node>`). A named-struct outType flattens to one
|
|
629
1096
|
* Go field per outType field; a scalar/arr/opt node carries a single `Val` payload. Every row carries
|
|
630
|
-
*
|
|
1097
|
+
* A row models a SUCCESS; failure is the second (error) return, so no in-band error signal.
|
|
631
1098
|
*/
|
|
632
1099
|
function emitRawRowStructs(comp, typedNodes, plan) {
|
|
633
1100
|
const parts = [];
|
|
@@ -642,7 +1109,7 @@ function emitRawRowStructs(comp, typedNodes, plan) {
|
|
|
642
1109
|
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
643
1110
|
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
644
1111
|
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
|
|
645
|
-
`type ${rawRowStructName(comp.name, n.id)} struct {\n\
|
|
1112
|
+
`type ${rawRowStructName(comp.name, n.id)} struct {\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
|
|
646
1113
|
continue;
|
|
647
1114
|
}
|
|
648
1115
|
if ("map" in n) {
|
|
@@ -655,7 +1122,7 @@ function emitRawRowStructs(comp, typedNodes, plan) {
|
|
|
655
1122
|
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
656
1123
|
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
657
1124
|
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
|
|
658
|
-
`type ${rawRowStructName(comp.name, n.id)} struct {\n\
|
|
1125
|
+
`type ${rawRowStructName(comp.name, n.id)} struct {\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
|
|
659
1126
|
continue;
|
|
660
1127
|
}
|
|
661
1128
|
parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
|
|
@@ -663,13 +1130,11 @@ function emitRawRowStructs(comp, typedNodes, plan) {
|
|
|
663
1130
|
return parts.join("\n\n");
|
|
664
1131
|
}
|
|
665
1132
|
/** Emit one concrete row struct: flatten a named outType to typed fields, else a single Val payload;
|
|
666
|
-
*
|
|
1133
|
+
* A row models a SUCCESS; a failure is the second (error) return, so no in-band error signal. */
|
|
667
1134
|
function emitOneRowStruct(name, ref, plan) {
|
|
668
1135
|
const lines = [];
|
|
669
|
-
lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType
|
|
1136
|
+
lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType).`);
|
|
670
1137
|
lines.push(`type ${name} struct {`);
|
|
671
|
-
lines.push(`\tIsError bool`);
|
|
672
|
-
lines.push(`\tErr string`);
|
|
673
1138
|
if (ref.kind === "named") {
|
|
674
1139
|
const decl = findDecl(plan, ref.name);
|
|
675
1140
|
for (const f of decl.fields) {
|
|
@@ -744,21 +1209,32 @@ function emitHandlerInterface(comp, typedNodes, plan) {
|
|
|
744
1209
|
const bt = boundGoType(n, comp, typedNodes, plan);
|
|
745
1210
|
if ("fanout" in n) {
|
|
746
1211
|
// fanout (v3): one batched dispatch of the deduped id list. Ports = the batch struct; returns the
|
|
747
|
-
// aligned
|
|
1212
|
+
// aligned per-body element WIRES ([]WireRow) that the runner de-boxes (decodeWireElem_*) then
|
|
1213
|
+
// applies dedupe/drop/wrap to. A non-record element keeps its concrete batch-row return.
|
|
748
1214
|
const portsT = `${portsStructName(comp.name, n.id)}_batch`;
|
|
749
|
-
const
|
|
750
|
-
|
|
1215
|
+
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
1216
|
+
const retT = elemDeBox ? "[]WireRow" : rawRowStructName(comp.name, n.id);
|
|
1217
|
+
lines.push(`\t${method}(ports ${portsT}, bound ${bt}) (${retT}, error)`);
|
|
751
1218
|
continue;
|
|
752
1219
|
}
|
|
753
1220
|
if ("map" in n) {
|
|
754
1221
|
const m = n.map;
|
|
755
1222
|
const batched = m.batched === true;
|
|
756
1223
|
const portsT = batched ? `${portsStructName(comp.name, n.id)}_batch` : portsStructName(comp.name, n.id);
|
|
757
|
-
|
|
758
|
-
|
|
1224
|
+
// a record-element map returns the element WIRE(s) the runner de-boxes: batched → []WireRow (aligned
|
|
1225
|
+
// to items), nonbatched → one WireRow per dispatch. A non-record element keeps its concrete row.
|
|
1226
|
+
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
1227
|
+
const retT = elemDeBox ? (batched ? "[]WireRow" : "WireRow") : batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
|
|
1228
|
+
lines.push(`\t${method}(ports ${portsT}, bound ${bt}) (${retT}, error)`);
|
|
759
1229
|
continue;
|
|
760
1230
|
}
|
|
761
|
-
|
|
1231
|
+
// a componentRef whose result is a record-containing top returns the wire (WireRow / []WireRow /
|
|
1232
|
+
// nilable WireRow / map[string]WireRow); the generated de-box (decodeWireResult_*) probes each record
|
|
1233
|
+
// strictly. A pure-scalar top (arr(scalar)/opt(scalar)/…) keeps its concrete row for direct copy.
|
|
1234
|
+
const ref = typedNodes.get(n.id);
|
|
1235
|
+
const top = ref !== undefined ? recordTop(ref) : null;
|
|
1236
|
+
const retT = top !== null ? goWireTypeForTop(top) : rawRowStructName(comp.name, n.id);
|
|
1237
|
+
lines.push(`\t${method}(ports ${portsStructName(comp.name, n.id)}, bound ${bt}) (${retT}, error)`);
|
|
762
1238
|
}
|
|
763
1239
|
lines.push(`}`);
|
|
764
1240
|
return lines.join("\n");
|
|
@@ -782,7 +1258,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
|
|
|
782
1258
|
// struct element). A pure-static/concat port ignores the binding.
|
|
783
1259
|
const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
784
1260
|
const asBinding = { name: m.as, ref: elemRef, plan };
|
|
785
|
-
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan);
|
|
1261
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan, typedNodes);
|
|
786
1262
|
const batchStruct = `${structName}_batch`;
|
|
787
1263
|
return `${elemStruct}
|
|
788
1264
|
|
|
@@ -800,7 +1276,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
|
|
|
800
1276
|
const structName = portsStructName(comp.name, node.id);
|
|
801
1277
|
const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
|
|
802
1278
|
const asBinding = { name: f.as, ref: elemRef, plan };
|
|
803
|
-
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan);
|
|
1279
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan, typedNodes);
|
|
804
1280
|
const batchStruct = `${structName}_batch`;
|
|
805
1281
|
return `${elemStruct}
|
|
806
1282
|
|
|
@@ -846,18 +1322,16 @@ function fanoutOverElemInfo(comp, node, typedNodes, plan) {
|
|
|
846
1322
|
}
|
|
847
1323
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go fanout (v3): fanout '${node.id}' 'over' must be a ref to a prior node's typed arr field OR an input array port with a declared elemType (got ${JSON.stringify(f.over)}).`);
|
|
848
1324
|
}
|
|
849
|
-
/** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
* Both converge to the SAME produced-array ref, so existing goldens stay byte-identical. */
|
|
1325
|
+
/** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType`.
|
|
1326
|
+
*
|
|
1327
|
+
* A map node's `outType` is its ELEMENT type (scp-error.md "Map element type"), so the produced array
|
|
1328
|
+
* is `{arr: element}`. This is the same reading the compiler applies when it resolves what a node
|
|
1329
|
+
* result reference has (`type-gate`: `"map" in n ? {arr:ot} : ot`) — the emitter follows that SSoT
|
|
1330
|
+
* rather than inspecting the annotation's shape, so an element that is ITSELF an `arr` stays
|
|
1331
|
+
* expressible (element `arr(T)` produces `arr(arr(T))`). */
|
|
857
1332
|
function mapNodeArrRef(node, plan) {
|
|
858
1333
|
const outT = node.outType;
|
|
859
|
-
|
|
860
|
-
return deriveTypeRef((isArr ? outT : { arr: outT }), plan);
|
|
1334
|
+
return deriveTypeRef({ arr: outT }, plan);
|
|
861
1335
|
}
|
|
862
1336
|
/** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
|
|
863
1337
|
* type (into) or the element outType directly (no-into). This is the type of RawElem_<comp>_<node>. */
|
|
@@ -947,43 +1421,6 @@ function mapOverElemInfo(comp, node, typedNodes, plan) {
|
|
|
947
1421
|
function mapOverElemType(comp, node, typedNodes, plan) {
|
|
948
1422
|
return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
|
|
949
1423
|
}
|
|
950
|
-
/**
|
|
951
|
-
* priorNodeArrElemInfo (#129) — resolve a leaf port that is a `ref` into a PRIOR BODY NODE whose
|
|
952
|
-
* annotated `outType` is (or reaches, through a field path) a native array, to its element TypeRef +
|
|
953
|
-
* the native Go slice expression that yields it. This GENERALIZES the `map…over` prior-node-arr
|
|
954
|
-
* dataflow (mapOverElemInfo's first branch) to ANY leaf input port: a fold/dedup leaf whose `items`
|
|
955
|
-
* port is `{ref:["<priorNode>"]}` (or `.items` of a prior connection node) now lowers to `[]ElemT`
|
|
956
|
-
* fed straight from the prior node's typed local — ZERO boxed Value on the read hot path.
|
|
957
|
-
*
|
|
958
|
-
* The element type comes STRICTLY from the prior node's EXPLICIT `outType` annotation walked through
|
|
959
|
-
* typedFieldAccess (BC does NOT infer — consumer-interface C3 / [[graphddb-typed-native-static-link]]):
|
|
960
|
-
* if the head is not a prior body node, or its outType (through the field path) is not an `arr`, this
|
|
961
|
-
* returns null and the caller keeps its existing fail-closed behaviour (never a silent box). A
|
|
962
|
-
* single-segment INPUT-array port (elemType) is a DIFFERENT covered shape (portArrayElemRef) — handled
|
|
963
|
-
* ahead of this. Requires the typedNodes map (prior-node outType TypeRefs) and a TypePlan.
|
|
964
|
-
*/
|
|
965
|
-
function priorNodeArrElemInfo(node, typedNodes, plan) {
|
|
966
|
-
if (typedNodes === undefined || plan === undefined)
|
|
967
|
-
return null;
|
|
968
|
-
if (staticArrElems(node) !== null)
|
|
969
|
-
return null; // a static array literal — not a prior-node ref.
|
|
970
|
-
const e = classifyExpr(node);
|
|
971
|
-
if (e.kind !== "ref" || e.opt || e.path.length < 1)
|
|
972
|
-
return null;
|
|
973
|
-
if (!typedNodes.has(e.path[0]))
|
|
974
|
-
return null; // head is not a prior body node → not this shape.
|
|
975
|
-
const baseRef = typedNodes.get(e.path[0]);
|
|
976
|
-
let acc;
|
|
977
|
-
try {
|
|
978
|
-
acc = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
|
|
979
|
-
}
|
|
980
|
-
catch {
|
|
981
|
-
return null; // the field path does not resolve on the prior node's outType → fail-closed at caller.
|
|
982
|
-
}
|
|
983
|
-
if (acc.ref.kind !== "arr")
|
|
984
|
-
return null; // the prior-node result (through the path) is not an array.
|
|
985
|
-
return { elemRef: acc.ref.elem, overExpr: acc.expr };
|
|
986
|
-
}
|
|
987
1424
|
function classifyPort(node) {
|
|
988
1425
|
if (staticArrElems(node) !== null)
|
|
989
1426
|
return "static";
|
|
@@ -1037,31 +1474,17 @@ function reconstructG(e) {
|
|
|
1037
1474
|
function inStructName(compName) {
|
|
1038
1475
|
return `In_${sanitize(compName)}`;
|
|
1039
1476
|
}
|
|
1040
|
-
/**
|
|
1041
|
-
* inputPortGoType — the native Go type for an input port's declared type. Scalars lower to the concrete
|
|
1042
|
-
* scalar; `array`/`map` with a declared elemType to `[]ElemT` / `map[string]ElemT`; an OPTIONAL port
|
|
1043
|
-
* (`{opt:T}` → `required:false`) to `*T` — the native representation of "the value is absent" (nil).
|
|
1044
|
-
* A declared type with no native lowering stays the boxed Value.
|
|
1045
|
-
*
|
|
1046
|
-
* An OPTIONAL port whose inner type has no native lowering FAILS CLOSED: a boxed `Value` cannot carry
|
|
1047
|
-
* the presence distinction on the covered plane, and emitting the bare inner type would silently turn
|
|
1048
|
-
* "absent" into a zero value. A port whose optionality cannot be represented is never silently coerced.
|
|
1049
|
-
*/
|
|
1477
|
+
/** Go type for an input port's declared type (scalar → concrete; array+elemType → []ElemT; else Value). */
|
|
1050
1478
|
function inputPortGoType(schema, plan, where = "input port") {
|
|
1051
1479
|
const ref = inputPortTypeRef(schema, plan);
|
|
1052
1480
|
if (ref !== undefined)
|
|
1053
1481
|
return renderTypeRef(ref);
|
|
1054
1482
|
if (inputPortIsOptional(schema))
|
|
1055
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Go lowering, so "absent" has no native representation
|
|
1483
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Go lowering, so "absent" has no native representation. Declare an elemType for an array/map, or use a natively-lowerable scalar.`);
|
|
1056
1484
|
return `${PKG}.Value`;
|
|
1057
1485
|
}
|
|
1058
|
-
/** The scalar kind
|
|
1059
|
-
* An OPTIONAL port is deliberately NOT a scalar kind here: its native type is `*T`, and the callers of
|
|
1060
|
-
* this function build REQUIRED scalar slots (`in.<Field>` read straight into a non-opt port field).
|
|
1061
|
-
* Answering "int64" for an `{opt:"int"}` port is exactly how "absent" would silently become `0`. */
|
|
1486
|
+
/** The scalar kind an input port declares, or undefined if it is not a native scalar. */
|
|
1062
1487
|
function inputScalarKind(schema) {
|
|
1063
|
-
if (inputPortIsOptional(schema))
|
|
1064
|
-
return undefined;
|
|
1065
1488
|
switch (schema?.type) {
|
|
1066
1489
|
case "string":
|
|
1067
1490
|
case "literal": // a `"literal"` union is a constrained string (see inputPortGoType).
|
|
@@ -1086,6 +1509,8 @@ function emitInStruct(comp, plan) {
|
|
|
1086
1509
|
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
|
|
1087
1510
|
}
|
|
1088
1511
|
const names = ports.map((p) => goFieldName(p));
|
|
1512
|
+
// #139: inputPortGoType reads the inputPortTypeRef SSoT — an OPTIONAL port is `*T` (absent → nil); an
|
|
1513
|
+
// optional whose inner type has no native lowering FAILS CLOSED (never a boxed Value escape).
|
|
1089
1514
|
const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`));
|
|
1090
1515
|
const nameW = Math.max(0, ...names.map((n) => n.length));
|
|
1091
1516
|
const typeW = Math.max(0, ...types.map((t) => t.length));
|
|
@@ -1127,15 +1552,6 @@ function emitInDecoder(comp, plan) {
|
|
|
1127
1552
|
const scalar = inputScalarKind(schema);
|
|
1128
1553
|
const et = schema.elemType;
|
|
1129
1554
|
lines.push(`\tif v, ok := input.Get(${JSON.stringify(p)}); ok {`);
|
|
1130
|
-
if (inputPortIsOptional(schema)) {
|
|
1131
|
-
// An OPTIONAL port decodes into its native *T. materializeExpr's opt de-box IS the wire rule:
|
|
1132
|
-
// a nil Value (null) → nil (absent), any other value → a pointer to the materialized inner. A key
|
|
1133
|
-
// that never appears leaves the struct's zero value (nil) — the same absent value. Checked FIRST:
|
|
1134
|
-
// optionality wraps every inner type (an optional array is *[]T, not []T).
|
|
1135
|
-
lines.push(`\t\tin.${field} = ${goTypedInternals.materializeExpr("v", inputPortTypeRef(schema, plan), plan)}`);
|
|
1136
|
-
lines.push(`\t}`);
|
|
1137
|
-
continue;
|
|
1138
|
-
}
|
|
1139
1555
|
if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
1140
1556
|
// #108: decode the boxed Value array into the native []ElemT (test glue — materialize each element
|
|
1141
1557
|
// via the typed materializer; a non-array or non-conforming element is a fail-closed decode error).
|
|
@@ -1166,11 +1582,9 @@ function emitInDecoder(comp, plan) {
|
|
|
1166
1582
|
lines.push(`\t\tin.${field} = v`);
|
|
1167
1583
|
}
|
|
1168
1584
|
else {
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
lines.push(`\t\
|
|
1172
|
-
lines.push(`\t\t}`);
|
|
1173
|
-
lines.push(`\t\tin.${field} = t`);
|
|
1585
|
+
// #139: materialize via the inputPortTypeRef SSoT so an OPTIONAL scalar reads as *T (present → &v);
|
|
1586
|
+
// an absent optional never enters this block (Get !ok), leaving the field nil (not a required zero).
|
|
1587
|
+
lines.push(`\t\tin.${field} = ${goTypedInternals.materializeExpr("v", inputPortTypeRef(schema, plan), plan)}`);
|
|
1174
1588
|
}
|
|
1175
1589
|
lines.push(`\t}`);
|
|
1176
1590
|
}
|
|
@@ -1178,53 +1592,10 @@ function emitInDecoder(comp, plan) {
|
|
|
1178
1592
|
lines.push(`}`);
|
|
1179
1593
|
return lines.join("\n");
|
|
1180
1594
|
}
|
|
1181
|
-
/**
|
|
1182
|
-
*
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
* emitNativeScalar — lower a static port expr to a NATIVE Go expression of the concrete `want` scalar
|
|
1186
|
-
* type, or null if it cannot be statically proven that type. Literals: a string literal is native for
|
|
1187
|
-
* `want==="string"`, a bool literal for `want==="bool"`. A ref resolves via `resolveRef` (typed input
|
|
1188
|
-
* field / prior-node scalar / element binding). A concat lowers ONLY into `string` and only when EVERY
|
|
1189
|
-
* part is itself a native string (so the concat's strings-only TYPE_MISMATCH can never fire — the parts
|
|
1190
|
-
* are statically strings), emitting `p0 + p1 + …`. Returns null (fall back to the Value path) otherwise.
|
|
1191
|
-
*/
|
|
1192
|
-
function emitNativeScalar(node, want, resolveRef) {
|
|
1193
|
-
// a static arr is a Value port (never a scalar) — not native-lowerable here.
|
|
1194
|
-
if (staticArrElems(node) !== null)
|
|
1195
|
-
return null;
|
|
1196
|
-
const e = classifyExpr(node);
|
|
1197
|
-
switch (e.kind) {
|
|
1198
|
-
case "str":
|
|
1199
|
-
return want === "string" ? JSON.stringify(e.value) : null;
|
|
1200
|
-
case "bool":
|
|
1201
|
-
return want === "bool" ? (e.value ? "true" : "false") : null;
|
|
1202
|
-
case "null":
|
|
1203
|
-
return null; // a null literal is a Value(nil), never a required native scalar
|
|
1204
|
-
case "ref": {
|
|
1205
|
-
const sc = resolveRef(e.path[0], e.path.slice(1), e.opt);
|
|
1206
|
-
if (sc === null || sc.scalar !== want)
|
|
1207
|
-
return null;
|
|
1208
|
-
return sc.expr;
|
|
1209
|
-
}
|
|
1210
|
-
case "concat": {
|
|
1211
|
-
if (want !== "string")
|
|
1212
|
-
return null;
|
|
1213
|
-
const parts = [];
|
|
1214
|
-
for (const p of e.parts) {
|
|
1215
|
-
const sub = emitNativeScalar(reconstructG(p), "string", resolveRef);
|
|
1216
|
-
if (sub === null)
|
|
1217
|
-
return null;
|
|
1218
|
-
// parenthesize a `+` sub-expression so string concatenation binds correctly (a ref/literal is
|
|
1219
|
-
// atomic; a nested concat is already a single `a + b` — wrap to keep it one operand).
|
|
1220
|
-
parts.push(sub.includes(" + ") ? `(${sub})` : sub);
|
|
1221
|
-
}
|
|
1222
|
-
return parts.join(" + ");
|
|
1223
|
-
}
|
|
1224
|
-
default:
|
|
1225
|
-
return null;
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1595
|
+
/** A ref resolver for native lowering: given a ref head + remaining field path, return a native scalar
|
|
1596
|
+
* Go expression (typed input field / prior-node typed struct scalar / element binding field), or null
|
|
1597
|
+
* when the ref does not resolve to a REQUIRED (non-opt) native scalar (then the caller falls back to
|
|
1598
|
+
* the nvVE Value path — which keeps the runtime null/opt/TYPE_MISMATCH semantics). */
|
|
1228
1599
|
/**
|
|
1229
1600
|
* emitPortInit — emit the port field build for one port as a FULLY NATIVE Go expression (bc#90 /
|
|
1230
1601
|
* runtime-free). Every covered port lowers to a concrete Go value assigned straight into the ports
|
|
@@ -1236,66 +1607,11 @@ function emitNativeScalar(node, want, resolveRef) {
|
|
|
1236
1607
|
* plane — a boxed escape would re-introduce the bc-runtime coupling this module removes). Returns the
|
|
1237
1608
|
* struct field initializer (`Field: <expr>`). `resolveRef` resolves a ref head for the scalar path.
|
|
1238
1609
|
*/
|
|
1239
|
-
function emitPortInit(pn, expr,
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
if (optPort !== null)
|
|
1245
|
-
return `${fieldName}: ${optPort.expr}`;
|
|
1246
|
-
// #108-map: a port that is a ref to a `$as` element MAP/NAMED field (or an input map port) → the typed
|
|
1247
|
-
// composite value directly off the element/input (native map[string]V / struct — ZERO boxed Value).
|
|
1248
|
-
if (plan !== undefined) {
|
|
1249
|
-
const compExpr = emitCompositePortValue(expr, inputPorts ?? {}, asBinding, plan, elemBaseExpr);
|
|
1250
|
-
if (compExpr !== null)
|
|
1251
|
-
return `${fieldName}: ${compExpr}`;
|
|
1252
|
-
}
|
|
1253
|
-
// #110: a componentRef port bound to a runtime array input port (with elemType) → the native []ElemT,
|
|
1254
|
-
// read straight from the concrete input struct (`in.<Field>`) — ZERO boxed Value on the read hot path.
|
|
1255
|
-
// The over-elem/element-source case (map…over) already resolves via mapOverElemInfo; this is the plain
|
|
1256
|
-
// componentRef port (an IN-list / array-bound WHERE head).
|
|
1257
|
-
if (inputPorts !== undefined && portArrayElemRef(expr, inputPorts, plan) !== null) {
|
|
1258
|
-
const e = classifyExpr(expr);
|
|
1259
|
-
if (e.kind === "ref" && e.path.length === 1) {
|
|
1260
|
-
return `${fieldName}: in.${goFieldName(e.path[0])}`;
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
// #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) →
|
|
1264
|
-
// the native []ElemT read straight off the prior node's typed local (`t_<node>.Field…`) — ZERO boxed
|
|
1265
|
-
// Value on the read hot path. The over-source case (map…over) resolves via mapOverElemInfo; this is
|
|
1266
|
-
// the plain leaf array input port (a fold/dedup leaf whose `items` = a prior node's array result).
|
|
1267
|
-
const priorArr = priorNodeArrElemInfo(expr, typedNodes, plan);
|
|
1268
|
-
if (priorArr !== null)
|
|
1269
|
-
return `${fieldName}: ${priorArr.overExpr}`;
|
|
1270
|
-
// change #2: a static string array (projection) → a native `[]string{...}` literal.
|
|
1271
|
-
const strArr = staticStringArrElems(expr);
|
|
1272
|
-
if (strArr !== null) {
|
|
1273
|
-
if (fieldGoType !== "[]string") {
|
|
1274
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90): port '${pn}' is a static string array but its field type is '${fieldGoType}' (expected []string).`);
|
|
1275
|
-
}
|
|
1276
|
-
return `${fieldName}: []string{${strArr.map((s) => JSON.stringify(s)).join(", ")}}`;
|
|
1277
|
-
}
|
|
1278
|
-
// a bare number literal (e.g. Query `limit: 20`): the SSoT range check ran in numLiteralGoExpr —
|
|
1279
|
-
// emit the concrete native int64/float64 (unwrap the dslcontracts.Value(...) box: it was only there
|
|
1280
|
-
// for the old boxed path; the range check is what matters, and it already passed).
|
|
1281
|
-
const num = numLiteralGoExpr(expr);
|
|
1282
|
-
if (num !== null) {
|
|
1283
|
-
if (fieldGoType === "int64")
|
|
1284
|
-
return `${fieldName}: int64(${expr})`;
|
|
1285
|
-
if (fieldGoType === "float64")
|
|
1286
|
-
return `${fieldName}: float64(${expr})`;
|
|
1287
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90): port '${pn}' is a number literal but its field type is '${fieldGoType}'.`);
|
|
1288
|
-
}
|
|
1289
|
-
// string / bool / scalar-input / concat → native scalar expression (no Value, no `.(T)`).
|
|
1290
|
-
const scalarWant = fieldGoType;
|
|
1291
|
-
const native = emitNativeScalar(expr, scalarWant, resolveRef);
|
|
1292
|
-
if (native !== null) {
|
|
1293
|
-
return `${fieldName}: ${native}`;
|
|
1294
|
-
}
|
|
1295
|
-
// FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would
|
|
1296
|
-
// re-couple the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the
|
|
1297
|
-
// component is not native-eligible and must regenerate on the boxed straight-line path.
|
|
1298
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): port '${pn}' (${JSON.stringify(expr)}) does not lower to a native Go value of type '${fieldGoType}'. The covered read plane admits NO boxed dslcontracts.Value escape. ${GO_STATIC_PORT_FORMS} Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
|
|
1610
|
+
function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
|
|
1611
|
+
const c = compilePortNode(expr, comp, plan, typedNodes, isPrior, `port '${pn}'`, opts);
|
|
1612
|
+
if (c.stmts.length > 0)
|
|
1613
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): port '${pn}' ${JSON.stringify(expr)} lowers to a FALLIBLE expression (one that can raise INT_OVERFLOW / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS). Such an expression lowers to STATEMENTS carrying an early error-return, and a port is built inline in the ports-struct literal — a position that cannot host them. Use a non-fallible port expression.`);
|
|
1614
|
+
return `${goPortFieldName(pn)}: ${c.expr}`;
|
|
1299
1615
|
}
|
|
1300
1616
|
/**
|
|
1301
1617
|
* emitParallelStageArm (bc#87) — EXPLICIT static parallel orchestration for a real-concurrency stage.
|
|
@@ -1318,7 +1634,7 @@ function emitPortInit(pn, expr, fieldGoType, resolveRef, inputPorts, plan, typed
|
|
|
1318
1634
|
*
|
|
1319
1635
|
* NO RunPlan / []OpSpec / planSpec walk: the staging, the bound, and which ops are parallel are baked.
|
|
1320
1636
|
*/
|
|
1321
|
-
function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags
|
|
1637
|
+
function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags) {
|
|
1322
1638
|
const lines = [];
|
|
1323
1639
|
const ids = stage.map((i) => comp.body[i].id);
|
|
1324
1640
|
lines.push(`\t// ── bc#87 real-concurrency stage {${ids.map((x) => JSON.stringify(x)).join(", ")}} — static parallel`);
|
|
@@ -1355,9 +1671,17 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1355
1671
|
const node = comp.body[i];
|
|
1356
1672
|
const s = sanitize(node.id);
|
|
1357
1673
|
const structName = portsStructName(comp.name, comp.body[i].id);
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
1674
|
+
const memRef = typedNodes.get(comp.body[i].id);
|
|
1675
|
+
// a de-box member's goroutine returns the top's wire (WireRow / []WireRow / nilable WireRow /
|
|
1676
|
+
// map[string]WireRow, decoded in COMMIT); a non-de-box member returns its concrete row directly.
|
|
1677
|
+
const memTop = recordTop(memRef);
|
|
1678
|
+
if (memTop !== null) {
|
|
1679
|
+
lines.push(`\t\tvar wire_${s} ${goWireTypeForTop(memTop)}`);
|
|
1680
|
+
}
|
|
1681
|
+
else {
|
|
1682
|
+
lines.push(`\t\tvar row_${s} ${rawRowStructName(comp.name, comp.body[i].id)}`);
|
|
1683
|
+
}
|
|
1684
|
+
lines.push(`\t\tvar row_${s}Err error`);
|
|
1361
1685
|
lines.push(`\t\trun_${s} := false`);
|
|
1362
1686
|
lines.push(`\t\tvar ps_${s} ${structName}`);
|
|
1363
1687
|
lines.push(`\t\t_ = ps_${s}`);
|
|
@@ -1407,11 +1731,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1407
1731
|
}
|
|
1408
1732
|
}
|
|
1409
1733
|
const inits = [];
|
|
1410
|
-
const resolveRef = resolveRefAt(atPos);
|
|
1411
1734
|
for (const pn of Object.keys(node.ports)) {
|
|
1412
|
-
|
|
1413
|
-
const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
|
|
1414
|
-
inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef, comp.inputPorts, plan, typedNodes));
|
|
1735
|
+
inits.push(emitPortInit(pn, node.ports[pn], comp, plan, typedNodes, (h) => priorNodeAt(h, atPos)));
|
|
1415
1736
|
}
|
|
1416
1737
|
lines.push(`${ind}ps_${s} = ${structName}{${inits.join(", ")}}`);
|
|
1417
1738
|
lines.push(`${ind}run_${s} = true`);
|
|
@@ -1432,7 +1753,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1432
1753
|
lines.push(`\t\t\t\tdefer wg.Done()`);
|
|
1433
1754
|
lines.push(`\t\t\t\tdefer func() { <-sem }()`);
|
|
1434
1755
|
const boundArg = node.bindField !== undefined ? `bound_${s}` : "nil";
|
|
1435
|
-
|
|
1756
|
+
const resultVar = deBoxEligible(typedNodes.get(node.id)) ? `wire_${s}` : `row_${s}`;
|
|
1757
|
+
lines.push(`\t\t\t\t${resultVar}, row_${s}Err = handlers.${nodeMethodName(comp.name, node.id)}(ps_${s}, ${boundArg})`);
|
|
1436
1758
|
lines.push(`\t\t\t}()`);
|
|
1437
1759
|
lines.push(`\t\t}`);
|
|
1438
1760
|
}
|
|
@@ -1444,20 +1766,53 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1444
1766
|
const s = sanitize(node.id);
|
|
1445
1767
|
const ref = typedNodes.get(node.id);
|
|
1446
1768
|
const oc = `row_${s}`;
|
|
1769
|
+
const top = recordTop(ref);
|
|
1770
|
+
const namedTop = top !== null && top.kind === "named";
|
|
1771
|
+
const deBox = top !== null;
|
|
1772
|
+
// a NAMED top decodes in place (decodeWire_<Name> → oc → field copy); an arr/opt/map top assembles
|
|
1773
|
+
// via decodeWireResult_* (the same decode recursion, one per record) into the typed local directly.
|
|
1447
1774
|
lines.push(`\t\tif run_${s} {`);
|
|
1448
|
-
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}`);
|
|
1449
1775
|
if (meta.policy === "continue") {
|
|
1450
|
-
lines.push(`\t\t\tif
|
|
1451
|
-
|
|
1452
|
-
|
|
1776
|
+
lines.push(`\t\t\tif ${oc}Err == nil {`);
|
|
1777
|
+
if (namedTop) {
|
|
1778
|
+
lines.push(`\t\t\t\tif ${oc}, ${oc}DErr := ${goDecodeNamedName(top.innerName)}(wire_${s}); ${oc}DErr == nil {`);
|
|
1779
|
+
lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 5));
|
|
1780
|
+
lines.push(`\t\t\t\t\tproduced_${s} = true`);
|
|
1781
|
+
lines.push(`\t\t\t\t}`);
|
|
1782
|
+
}
|
|
1783
|
+
else if (deBox) {
|
|
1784
|
+
lines.push(`\t\t\t\tif res_${s}, res_${s}DErr := ${goDecodeResultName(comp.name, node.id)}(wire_${s}); res_${s}DErr == nil {`);
|
|
1785
|
+
lines.push(`\t\t\t\t\t${typedLocal(node.id)} = res_${s}`);
|
|
1786
|
+
lines.push(`\t\t\t\t\tproduced_${s} = true`);
|
|
1787
|
+
lines.push(`\t\t\t\t}`);
|
|
1788
|
+
}
|
|
1789
|
+
else {
|
|
1790
|
+
lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 4));
|
|
1791
|
+
lines.push(`\t\t\t\tproduced_${s} = true`);
|
|
1792
|
+
}
|
|
1453
1793
|
lines.push(`\t\t\t}`);
|
|
1454
1794
|
}
|
|
1455
1795
|
else {
|
|
1456
|
-
|
|
1457
|
-
lines.push(`\t\t\
|
|
1458
|
-
lines.push(`\t\t\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under '${policy}: "+${oc}.Err`)}`);
|
|
1796
|
+
lines.push(`\t\t\tif ${oc}Err != nil {`);
|
|
1797
|
+
lines.push(`\t\t\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, ${JSON.stringify(meta.policy === "retry" ? "retry" : "fail")}, ${oc}Err)`);
|
|
1459
1798
|
lines.push(`\t\t\t}`);
|
|
1460
|
-
|
|
1799
|
+
if (namedTop) {
|
|
1800
|
+
lines.push(`\t\t\t${oc}, ${oc}DErr := ${goDecodeNamedName(top.innerName)}(wire_${s})`);
|
|
1801
|
+
lines.push(`\t\t\tif ${oc}DErr != nil {`);
|
|
1802
|
+
lines.push(`\t\t\t\treturn ${zeroOut}, ${oc}DErr`);
|
|
1803
|
+
lines.push(`\t\t\t}`);
|
|
1804
|
+
lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 3));
|
|
1805
|
+
}
|
|
1806
|
+
else if (deBox) {
|
|
1807
|
+
lines.push(`\t\t\tres_${s}, res_${s}DErr := ${goDecodeResultName(comp.name, node.id)}(wire_${s})`);
|
|
1808
|
+
lines.push(`\t\t\tif res_${s}DErr != nil {`);
|
|
1809
|
+
lines.push(`\t\t\t\treturn ${zeroOut}, res_${s}DErr`);
|
|
1810
|
+
lines.push(`\t\t\t}`);
|
|
1811
|
+
lines.push(`\t\t\t${typedLocal(node.id)} = res_${s}`);
|
|
1812
|
+
}
|
|
1813
|
+
else {
|
|
1814
|
+
lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 3));
|
|
1815
|
+
}
|
|
1461
1816
|
lines.push(`\t\t\tproduced_${s} = true`);
|
|
1462
1817
|
}
|
|
1463
1818
|
lines.push(`\t\t}`);
|
|
@@ -1518,35 +1873,6 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1518
1873
|
// assert on a row value — the runner reads row.<Field> directly and copies each into the outType local.
|
|
1519
1874
|
lines.push(`func ${runnerName(comp.name)}[H ${handlerIfaceName(comp.name)}](handlers H, in ${inStructName(comp.name)}) (${outStructType}, error) {`);
|
|
1520
1875
|
lines.push(`\t_ = in`);
|
|
1521
|
-
// resolveNativeRef for this runner: a ref head is either a PRIOR NODE (read its typed struct field
|
|
1522
|
-
// directly) or an INPUT PORT (read the typed `in.<Field>`). A REQUIRED scalar resolves to a native
|
|
1523
|
-
// Go expression; an opt / non-scalar / refOpt resolves to null (falls back to the nvVE Value path).
|
|
1524
|
-
const resolveRefAt = (atPos) => (head, restPath, opt) => {
|
|
1525
|
-
if (opt)
|
|
1526
|
-
return null; // a refOpt may be nil at runtime — keep the Value path (opt-nil semantics)
|
|
1527
|
-
if (priorNodeAt(head, atPos)) {
|
|
1528
|
-
const baseRef = typedNodes.get(head);
|
|
1529
|
-
let acc;
|
|
1530
|
-
let expr;
|
|
1531
|
-
try {
|
|
1532
|
-
const r = goTypedInternals.typedFieldAccess(typedLocal(head), baseRef, restPath, plan);
|
|
1533
|
-
expr = r.expr;
|
|
1534
|
-
acc = r.ref;
|
|
1535
|
-
}
|
|
1536
|
-
catch {
|
|
1537
|
-
return null;
|
|
1538
|
-
}
|
|
1539
|
-
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1540
|
-
return null;
|
|
1541
|
-
return { expr, scalar: goScalar(acc.scalar) };
|
|
1542
|
-
}
|
|
1543
|
-
if (restPath.length !== 0)
|
|
1544
|
-
return null; // input.<field> path is a Value walk — not a native scalar
|
|
1545
|
-
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1546
|
-
if (scalar === undefined)
|
|
1547
|
-
return null;
|
|
1548
|
-
return { expr: `in.${goFieldName(head)}`, scalar };
|
|
1549
|
-
};
|
|
1550
1876
|
for (const k of order.keys()) {
|
|
1551
1877
|
const node = comp.body[order[k]];
|
|
1552
1878
|
const id = node.id;
|
|
@@ -1569,7 +1895,7 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1569
1895
|
// ascending-sorted stages), so we emit the block at the first member and skip the rest.
|
|
1570
1896
|
const stage = ep.parallelStageOf.get(i);
|
|
1571
1897
|
if (stage !== undefined && stage[0] === i) {
|
|
1572
|
-
lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags
|
|
1898
|
+
lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags));
|
|
1573
1899
|
k += stage.length - 1;
|
|
1574
1900
|
continue;
|
|
1575
1901
|
}
|
|
@@ -1641,30 +1967,80 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1641
1967
|
// dslcontracts.Value, ZERO nvVE helper, ZERO `.(T)`. A genuinely-dynamic port FAILS CLOSED.
|
|
1642
1968
|
const inits = [];
|
|
1643
1969
|
const boundArg = node.bindField !== undefined ? `bound_${s}` : "nil";
|
|
1644
|
-
const resolveRef = resolveRefAt(k);
|
|
1645
1970
|
for (const pn of portNames) {
|
|
1646
|
-
|
|
1647
|
-
const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
|
|
1648
|
-
inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef, comp.inputPorts, plan, typedNodes));
|
|
1971
|
+
inits.push(emitPortInit(pn, node.ports[pn], comp, plan, typedNodes, (h) => priorNodeAt(h, k)));
|
|
1649
1972
|
}
|
|
1650
1973
|
lines.push(`${indent}ports_${s} := ${structName}{${inits.join(", ")}}`);
|
|
1651
1974
|
const oc = `row_${s}`;
|
|
1652
|
-
lines.push(`${indent}${oc}, ${oc}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg})`);
|
|
1653
|
-
lines.push(`${indent}if !${oc}Resolved {`, `${indent}\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${node.component}' has no handler (fail-closed)"`)}`, `${indent}}`);
|
|
1654
1975
|
const copy = (ind) => emitRowCopy(typedLocal(node.id), oc, ref, plan, ind.length);
|
|
1976
|
+
// a NAMED top returns a WireRow, decoded in place (decodeWire_<Name> → oc → field copy). An arr/opt/map
|
|
1977
|
+
// top returns []WireRow / nilable WireRow / map[string]WireRow, assembled via decodeWireResult_* (the same
|
|
1978
|
+
// decode recursion, one per record). Either way a de-box mismatch fails closed with the structured Error
|
|
1979
|
+
// Value (byte-equal codes to run_behavior's outType check, so ≡ holds). A non-de-box node keeps its
|
|
1980
|
+
// direct concrete-row return + field copy.
|
|
1981
|
+
const top = recordTop(ref);
|
|
1982
|
+
const namedTop = top !== null && top.kind === "named";
|
|
1983
|
+
const deBox = top !== null;
|
|
1655
1984
|
if (meta.policy === "continue") {
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1985
|
+
// continue: a failed node (leaf failure OR de-box mismatch) produces no Port — the downstream
|
|
1986
|
+
// Skip propagation is what `continue` means. Only a clean decode commits the result.
|
|
1987
|
+
if (namedTop) {
|
|
1988
|
+
lines.push(`${indent}if wire_${s}, wire_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg}); wire_${s}Err == nil {`);
|
|
1989
|
+
lines.push(`${indent}\tif ${oc}, ${oc}DErr := ${goDecodeNamedName(top.innerName)}(wire_${s}); ${oc}DErr == nil {`);
|
|
1990
|
+
lines.push(copy(indent + "\t\t"));
|
|
1991
|
+
lines.push(`${indent}\t\tproduced_${s} = true`);
|
|
1992
|
+
lines.push(`${indent}\t}`);
|
|
1993
|
+
lines.push(`${indent}}`);
|
|
1994
|
+
}
|
|
1995
|
+
else if (deBox) {
|
|
1996
|
+
lines.push(`${indent}if wire_${s}, wire_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg}); wire_${s}Err == nil {`);
|
|
1997
|
+
lines.push(`${indent}\tif res_${s}, res_${s}DErr := ${goDecodeResultName(comp.name, node.id)}(wire_${s}); res_${s}DErr == nil {`);
|
|
1998
|
+
lines.push(`${indent}\t\t${typedLocal(node.id)} = res_${s}`);
|
|
1999
|
+
lines.push(`${indent}\t\tproduced_${s} = true`);
|
|
2000
|
+
lines.push(`${indent}\t}`);
|
|
2001
|
+
lines.push(`${indent}}`);
|
|
2002
|
+
}
|
|
2003
|
+
else {
|
|
2004
|
+
lines.push(`${indent}if ${oc}, ${oc}Err := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg}); ${oc}Err == nil {`);
|
|
2005
|
+
lines.push(copy(indent + "\t"));
|
|
2006
|
+
lines.push(`${indent}\tproduced_${s} = true`);
|
|
2007
|
+
lines.push(`${indent}}`);
|
|
2008
|
+
}
|
|
1660
2009
|
}
|
|
1661
2010
|
else {
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
2011
|
+
const policyLit = JSON.stringify(meta.policy === "retry" ? "retry" : "fail");
|
|
2012
|
+
if (namedTop) {
|
|
2013
|
+
lines.push(`${indent}wire_${s}, wire_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg})`);
|
|
2014
|
+
lines.push(`${indent}if wire_${s}Err != nil {`);
|
|
2015
|
+
lines.push(`${indent}\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, ${policyLit}, wire_${s}Err)`);
|
|
2016
|
+
lines.push(`${indent}}`);
|
|
2017
|
+
lines.push(`${indent}${oc}, ${oc}DErr := ${goDecodeNamedName(top.innerName)}(wire_${s})`);
|
|
2018
|
+
lines.push(`${indent}if ${oc}DErr != nil {`);
|
|
2019
|
+
lines.push(`${indent}\treturn ${zeroOut}, ${oc}DErr`);
|
|
2020
|
+
lines.push(`${indent}}`);
|
|
2021
|
+
lines.push(copy(indent));
|
|
2022
|
+
lines.push(`${indent}produced_${s} = true`);
|
|
2023
|
+
}
|
|
2024
|
+
else if (deBox) {
|
|
2025
|
+
lines.push(`${indent}wire_${s}, wire_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg})`);
|
|
2026
|
+
lines.push(`${indent}if wire_${s}Err != nil {`);
|
|
2027
|
+
lines.push(`${indent}\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, ${policyLit}, wire_${s}Err)`);
|
|
2028
|
+
lines.push(`${indent}}`);
|
|
2029
|
+
lines.push(`${indent}res_${s}, res_${s}DErr := ${goDecodeResultName(comp.name, node.id)}(wire_${s})`);
|
|
2030
|
+
lines.push(`${indent}if res_${s}DErr != nil {`);
|
|
2031
|
+
lines.push(`${indent}\treturn ${zeroOut}, res_${s}DErr`);
|
|
2032
|
+
lines.push(`${indent}}`);
|
|
2033
|
+
lines.push(`${indent}${typedLocal(node.id)} = res_${s}`);
|
|
2034
|
+
lines.push(`${indent}produced_${s} = true`);
|
|
2035
|
+
}
|
|
2036
|
+
else {
|
|
2037
|
+
lines.push(`${indent}${oc}, ${oc}Err := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg})`);
|
|
2038
|
+
lines.push(`${indent}if ${oc}Err != nil {`);
|
|
2039
|
+
lines.push(`${indent}\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, ${policyLit}, ${oc}Err)`);
|
|
2040
|
+
lines.push(`${indent}}`);
|
|
2041
|
+
lines.push(copy(indent));
|
|
2042
|
+
lines.push(`${indent}produced_${s} = true`);
|
|
2043
|
+
}
|
|
1668
2044
|
}
|
|
1669
2045
|
for (const c of closers)
|
|
1670
2046
|
lines.push(c);
|
|
@@ -1738,67 +2114,16 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1738
2114
|
const mkGuard = (elemVar) => {
|
|
1739
2115
|
if (!hasGuard)
|
|
1740
2116
|
return null;
|
|
1741
|
-
const resolveHead = (
|
|
1742
|
-
if (head === asName)
|
|
1743
|
-
return { expr: elemVar, ref: overElemRef };
|
|
1744
|
-
if (priorHere(head))
|
|
1745
|
-
return { expr: typedLocal(head), ref: typedNodes.get(head) };
|
|
1746
|
-
return goInputHeadRef(head, comp, plan);
|
|
1747
|
-
};
|
|
2117
|
+
const resolveHead = goNativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding: { name: asName, ref: overElemRef, plan }, asBase: elemVar });
|
|
1748
2118
|
const be = makeGoExprBackend(zeroOut, resolveHead, plan);
|
|
1749
2119
|
return new NativeExprCompiler(be).compileBool(m.when);
|
|
1750
2120
|
};
|
|
1751
2121
|
// emit the per-element native ports struct build (shared; `il` is the indent inside the element
|
|
1752
2122
|
// loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
|
|
1753
2123
|
const buildPorts = (il, elemVar) => {
|
|
1754
|
-
const out = [];
|
|
1755
|
-
const inits = [];
|
|
1756
|
-
// resolveNativeRef for a map element port: a ref head is the element binding ($as → the over element
|
|
1757
|
-
// typed struct field `elemVar.<Field>`), a PRIOR NODE (its typed struct field), or an INPUT PORT
|
|
1758
|
-
// (`in.<Field>`). A REQUIRED scalar lowers to a native Go expr; opt / non-scalar → null (Value path).
|
|
1759
|
-
const resolveRef = (head, restPath, opt) => {
|
|
1760
|
-
if (opt)
|
|
1761
|
-
return null;
|
|
1762
|
-
let baseExpr;
|
|
1763
|
-
let baseRef;
|
|
1764
|
-
if (head === asName) {
|
|
1765
|
-
baseExpr = elemVar;
|
|
1766
|
-
baseRef = overElemRef;
|
|
1767
|
-
}
|
|
1768
|
-
else if (priorHere(head)) {
|
|
1769
|
-
baseExpr = typedLocal(head);
|
|
1770
|
-
baseRef = typedNodes.get(head);
|
|
1771
|
-
}
|
|
1772
|
-
else {
|
|
1773
|
-
if (restPath.length !== 0)
|
|
1774
|
-
return null;
|
|
1775
|
-
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1776
|
-
if (scalar === undefined)
|
|
1777
|
-
return null;
|
|
1778
|
-
return { expr: `in.${goFieldName(head)}`, scalar };
|
|
1779
|
-
}
|
|
1780
|
-
let acc;
|
|
1781
|
-
let expr;
|
|
1782
|
-
try {
|
|
1783
|
-
const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
|
|
1784
|
-
expr = r.expr;
|
|
1785
|
-
acc = r.ref;
|
|
1786
|
-
}
|
|
1787
|
-
catch {
|
|
1788
|
-
return null;
|
|
1789
|
-
}
|
|
1790
|
-
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1791
|
-
return null;
|
|
1792
|
-
return { expr, scalar: goScalar(acc.scalar) };
|
|
1793
|
-
};
|
|
1794
2124
|
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
// #108-map: pass the element binding + base expr so a `$as` MAP/NAMED field port reads the typed composite.
|
|
1798
|
-
inits.push(emitPortInit(pn, m.ports[pn], fieldGoType, resolveRef, comp.inputPorts, plan, undefined, asBinding, elemLocal(s)));
|
|
1799
|
-
}
|
|
1800
|
-
out.push(`${il}ep_${s} := ${structName}{${inits.join(", ")}}`);
|
|
1801
|
-
return out;
|
|
2125
|
+
const inits = portNames.map((pn) => emitPortInit(pn, m.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: elemVar }));
|
|
2126
|
+
return [`${il}ep_${s} := ${structName}{${inits.join(", ")}}`];
|
|
1802
2127
|
};
|
|
1803
2128
|
const lines = [];
|
|
1804
2129
|
const closers = [];
|
|
@@ -1814,6 +2139,12 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1814
2139
|
lines.push(`${indent}over_${s} := ${overExpr}`);
|
|
1815
2140
|
lines.push(`${indent}${typedLocal(node.id)} = make(${elemGo}, 0, len(over_${s}))`);
|
|
1816
2141
|
const elemRowRef = mapElemRowRef(node, arrRef, plan);
|
|
2142
|
+
// a record element is de-boxed strictly: the handler returns the element WIRE and the arm decodes it via
|
|
2143
|
+
// decodeWireElem_* (closing the scan-row silent-default hole). A de-box mismatch fails closed regardless
|
|
2144
|
+
// of elementPolicy (matching the interpreter's whole-node outType throw); elementPolicy still governs the
|
|
2145
|
+
// handler's own leaf failures only.
|
|
2146
|
+
const elemDeBox = elemRowRef.kind === "named";
|
|
2147
|
+
const decElemFn = elemDeBox ? goDecodeElemName(comp.name, node.id) : "";
|
|
1817
2148
|
// emit the per-element guard keep-gate (#108). `il` = indent, `elemVar` = the over element loop var.
|
|
1818
2149
|
// On skip: batched → `continue` (element excluded from items + keptOver); non-batched → `continue`.
|
|
1819
2150
|
const emitGuard = (il, elemVar) => {
|
|
@@ -1830,7 +2161,7 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1830
2161
|
};
|
|
1831
2162
|
if (batched) {
|
|
1832
2163
|
// BATCHED (#86 pt2 into / #93 shape 3 no-into / #108 guarded no-into): build kept-element ports,
|
|
1833
|
-
// dispatch ONCE. The concrete Node_* returns RawRow{
|
|
2164
|
+
// dispatch ONCE. The concrete Node_* returns RawRow{Rows []RawElem} aligned to items (or an error).
|
|
1834
2165
|
// With a guard, `items` (and, for into, `keptOver`) hold only kept elements — matching run_behavior's
|
|
1835
2166
|
// keptIdx-aligned batch. `into` is never combined with a guard (rejected). no-into collects each row.
|
|
1836
2167
|
lines.push(`${indent}items_${s} := make([]${structName}, 0, len(over_${s}))`);
|
|
@@ -1848,22 +2179,39 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1848
2179
|
// empty items => empty result (handler not called), matching run_behavior.
|
|
1849
2180
|
lines.push(`${indent}if len(items_${s}) > 0 {`);
|
|
1850
2181
|
lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: items_${s}}`);
|
|
1851
|
-
lines.push(`${indent}\tmo_${s}, mo_${s}
|
|
1852
|
-
lines.push(`${indent}\tif
|
|
1853
|
-
|
|
1854
|
-
|
|
2182
|
+
lines.push(`${indent}\tmo_${s}, mo_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
|
|
2183
|
+
lines.push(`${indent}\tif mo_${s}Err != nil {`, `${indent}\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, "fail", mo_${s}Err)`, `${indent}\t}`);
|
|
2184
|
+
// the batch length aligns to items (the handler returns the element WIRES aligned; a record element
|
|
2185
|
+
// is de-boxed below, a non-record element is the concrete batch row).
|
|
2186
|
+
const batchLen = elemDeBox ? `len(mo_${s})` : `len(mo_${s}.Rows)`;
|
|
2187
|
+
const batchAt = (mk) => (elemDeBox ? `mo_${s}[${mk}]` : `mo_${s}.Rows[${mk}]`);
|
|
2188
|
+
lines.push(`${indent}\tif ${batchLen} != 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}`);
|
|
1855
2189
|
if (hasInto) {
|
|
1856
2190
|
// #86 pt2: zip-attach the into field onto each kept over element (augmented element copier).
|
|
1857
2191
|
lines.push(`${indent}\tfor mk_${s} := range keptOver_${s} {`);
|
|
1858
|
-
|
|
2192
|
+
if (elemDeBox) {
|
|
2193
|
+
lines.push(`${indent}\t\telm_${s}, elm_${s}Err := ${decElemFn}(${batchAt(`mk_${s}`)})`);
|
|
2194
|
+
lines.push(`${indent}\t\tif elm_${s}Err != nil {`, `${indent}\t\t\treturn ${zeroOut}, elm_${s}Err`, `${indent}\t\t}`);
|
|
2195
|
+
lines.push(`${indent}\t\tel_${s} := ${elemMat}(keptOver_${s}[mk_${s}], elm_${s})`);
|
|
2196
|
+
}
|
|
2197
|
+
else {
|
|
2198
|
+
lines.push(`${indent}\t\tel_${s} := ${elemMat}(keptOver_${s}[mk_${s}], ${batchAt(`mk_${s}`)})`);
|
|
2199
|
+
}
|
|
1859
2200
|
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1860
2201
|
lines.push(`${indent}\t}`);
|
|
1861
2202
|
}
|
|
1862
2203
|
else {
|
|
1863
2204
|
// #93 shape 3 / #108: collect each element DIRECTLY from the aligned per-element row (field copy).
|
|
1864
|
-
lines.push(`${indent}\tfor mk_${s} :=
|
|
2205
|
+
lines.push(`${indent}\tfor mk_${s} := 0; mk_${s} < ${batchLen}; mk_${s}++ {`);
|
|
1865
2206
|
lines.push(`${indent}\t\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
|
|
1866
|
-
|
|
2207
|
+
if (elemDeBox) {
|
|
2208
|
+
lines.push(`${indent}\t\telm_${s}, elm_${s}Err := ${decElemFn}(${batchAt(`mk_${s}`)})`);
|
|
2209
|
+
lines.push(`${indent}\t\tif elm_${s}Err != nil {`, `${indent}\t\t\treturn ${zeroOut}, elm_${s}Err`, `${indent}\t\t}`);
|
|
2210
|
+
lines.push(emitRowCopy(`el_${s}`, `elm_${s}`, elemRowRef, plan, indent.length + 2));
|
|
2211
|
+
}
|
|
2212
|
+
else {
|
|
2213
|
+
lines.push(emitRowCopy(`el_${s}`, batchAt(`mk_${s}`), elemRowRef, plan, indent.length + 2));
|
|
2214
|
+
}
|
|
1867
2215
|
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1868
2216
|
lines.push(`${indent}\t}`);
|
|
1869
2217
|
}
|
|
@@ -1882,15 +2230,30 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1882
2230
|
// per-element dispatch (ONE physical request per element). run_behavior passes the over element
|
|
1883
2231
|
// as the bound value; the element data is fully conveyed by the native ports struct here, so bound
|
|
1884
2232
|
// stays nil. Byte-equivalence holds because the bound value only feeds handler-internal logic.
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
2233
|
+
// Element Error Policy (scp-error.md): `skip` drops the failing element and proceeds (order is
|
|
2234
|
+
// preserved; the leaf keeps the Error Value it produced). `error` (default) promotes the element
|
|
2235
|
+
// Failure to the map's Component Failure, carrying the leaf's detail through.
|
|
2236
|
+
lines.push(`${indent}\tmo_${s}, mo_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(ep_${s}, nil)`);
|
|
2237
|
+
if (m.elementPolicy === "skip") {
|
|
2238
|
+
lines.push(`${indent}\tif mo_${s}Err != nil {`, `${indent}\t\tcontinue`, `${indent}\t}`);
|
|
2239
|
+
}
|
|
2240
|
+
else {
|
|
2241
|
+
lines.push(`${indent}\tif mo_${s}Err != nil {`, `${indent}\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, "fail", mo_${s}Err)`, `${indent}\t}`);
|
|
2242
|
+
}
|
|
2243
|
+
// a record element's wire is de-boxed strictly. A de-box mismatch fails closed regardless of
|
|
2244
|
+
// elementPolicy (≡ the interpreter's whole-node outType throw); elementPolicy governs the handler's
|
|
2245
|
+
// own leaf failures only (above).
|
|
2246
|
+
const elemVar = elemDeBox ? `elm_${s}` : `mo_${s}`;
|
|
2247
|
+
if (elemDeBox) {
|
|
2248
|
+
lines.push(`${indent}\telm_${s}, elm_${s}Err := ${decElemFn}(mo_${s})`);
|
|
2249
|
+
lines.push(`${indent}\tif elm_${s}Err != nil {`, `${indent}\t\treturn ${zeroOut}, elm_${s}Err`, `${indent}\t}`);
|
|
2250
|
+
}
|
|
1888
2251
|
if (hasInto) {
|
|
1889
|
-
lines.push(`${indent}\tel_${s} := ${elemMat}(${elemLocal(s)},
|
|
2252
|
+
lines.push(`${indent}\tel_${s} := ${elemMat}(${elemLocal(s)}, ${elemVar})`);
|
|
1890
2253
|
}
|
|
1891
2254
|
else {
|
|
1892
2255
|
lines.push(`${indent}\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
|
|
1893
|
-
lines.push(emitRowCopy(`el_${s}`,
|
|
2256
|
+
lines.push(emitRowCopy(`el_${s}`, elemVar, elemRowRef, plan, indent.length + 1));
|
|
1894
2257
|
}
|
|
1895
2258
|
lines.push(`${indent}\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1896
2259
|
lines.push(`${indent}}`);
|
|
@@ -1936,47 +2299,8 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
|
|
|
1936
2299
|
const dkGo = goFieldName(f.dedupeKey);
|
|
1937
2300
|
// per-id native ports build (same as a batched map's per-element ports). $as → the over id value.
|
|
1938
2301
|
const buildPorts = (il, elemVar) => {
|
|
1939
|
-
const inits = [];
|
|
1940
|
-
const resolveRef = (head, restPath, opt) => {
|
|
1941
|
-
if (opt)
|
|
1942
|
-
return null;
|
|
1943
|
-
let baseExpr;
|
|
1944
|
-
let baseRef;
|
|
1945
|
-
if (head === asName) {
|
|
1946
|
-
baseExpr = elemVar;
|
|
1947
|
-
baseRef = overElemRef;
|
|
1948
|
-
}
|
|
1949
|
-
else if (priorHere(head)) {
|
|
1950
|
-
baseExpr = typedLocal(head);
|
|
1951
|
-
baseRef = typedNodes.get(head);
|
|
1952
|
-
}
|
|
1953
|
-
else {
|
|
1954
|
-
if (restPath.length !== 0)
|
|
1955
|
-
return null;
|
|
1956
|
-
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1957
|
-
if (scalar === undefined)
|
|
1958
|
-
return null;
|
|
1959
|
-
return { expr: `in.${goFieldName(head)}`, scalar };
|
|
1960
|
-
}
|
|
1961
|
-
let acc;
|
|
1962
|
-
let expr;
|
|
1963
|
-
try {
|
|
1964
|
-
const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
|
|
1965
|
-
expr = r.expr;
|
|
1966
|
-
acc = r.ref;
|
|
1967
|
-
}
|
|
1968
|
-
catch {
|
|
1969
|
-
return null;
|
|
1970
|
-
}
|
|
1971
|
-
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1972
|
-
return null;
|
|
1973
|
-
return { expr, scalar: goScalar(acc.scalar) };
|
|
1974
|
-
};
|
|
1975
2302
|
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1976
|
-
|
|
1977
|
-
const fieldGoType = portFieldGoType(f.ports[pn], comp.inputPorts, `fanout port '${pn}' of node '${node.id}'`, asBinding, plan);
|
|
1978
|
-
inits.push(emitPortInit(pn, f.ports[pn], fieldGoType, resolveRef, comp.inputPorts, plan));
|
|
1979
|
-
}
|
|
2303
|
+
const inits = portNames.map((pn) => emitPortInit(pn, f.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: elemVar }));
|
|
1980
2304
|
return [`${il}ep_${s} := ${structName}{${inits.join(", ")}}`];
|
|
1981
2305
|
};
|
|
1982
2306
|
const lines = [];
|
|
@@ -2000,19 +2324,25 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
|
|
|
2000
2324
|
// empty id-list => empty connection (handler not called), matching run_behavior.
|
|
2001
2325
|
lines.push(`${indent}if len(pi_${s}) > 0 {`);
|
|
2002
2326
|
lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: pi_${s}}`);
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2327
|
+
// the fanout element row is a named record → the handler returns the aligned element WIRES ([]WireRow),
|
|
2328
|
+
// and each is de-boxed strictly via decodeWireElem_* (a de-box mismatch fails closed). A DANGLING id is
|
|
2329
|
+
// a nil wire → the zero element (empty dedupeKey), which the dangling drop below excludes.
|
|
2330
|
+
const decElemFn = goDecodeElemName(comp.name, node.id);
|
|
2331
|
+
lines.push(`${indent}\tfo_${s}, fo_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
|
|
2332
|
+
lines.push(`${indent}\tif fo_${s}Err != nil {`, `${indent}\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, "fail", fo_${s}Err)`, `${indent}\t}`);
|
|
2333
|
+
lines.push(`${indent}\tif len(fo_${s}) != len(pi_${s}) {`, `${indent}\t\treturn ${zeroOut}, ${goErr("FANOUT_BATCH_RESULT_MISMATCH", `fmt.Sprintf("fanout '${node.id}': batched handler must return a list aligned to the deduped id list (want %d)", len(pi_${s}))`)}`, `${indent}\t}`);
|
|
2007
2334
|
// ── THE ONE dedupe/drop (mirrors fanoutDedupDrop verbatim): first-seen dedupe by dedupeKey field;
|
|
2008
2335
|
// drop dangling (zero dedupeKey ≡ interpreter's null/absent-key body); implicitSource strip is
|
|
2009
2336
|
// structural (the elem type already omits it). cursor is always nil (connection wrap).
|
|
2010
|
-
lines.push(`${indent}\tseen_${s} := make(map[string]struct{}, len(fo_${s}
|
|
2011
|
-
lines.push(`${indent}\titems_${s} = make([]${elemGo}, 0, len(fo_${s}
|
|
2012
|
-
lines.push(`${indent}\tfor fi_${s} := range fo_${s}
|
|
2013
|
-
lines.push(`${indent}\t\tbody_${s} := fo_${s}.Rows[fi_${s}]`);
|
|
2337
|
+
lines.push(`${indent}\tseen_${s} := make(map[string]struct{}, len(fo_${s}))`);
|
|
2338
|
+
lines.push(`${indent}\titems_${s} = make([]${elemGo}, 0, len(fo_${s}))`);
|
|
2339
|
+
lines.push(`${indent}\tfor fi_${s} := range fo_${s} {`);
|
|
2014
2340
|
lines.push(`${indent}\t\tvar el_${s} ${elemGo}`);
|
|
2015
|
-
lines.push(
|
|
2341
|
+
lines.push(`${indent}\t\tif fo_${s}[fi_${s}] != nil {`);
|
|
2342
|
+
lines.push(`${indent}\t\t\telm_${s}, elm_${s}Err := ${decElemFn}(fo_${s}[fi_${s}])`);
|
|
2343
|
+
lines.push(`${indent}\t\t\tif elm_${s}Err != nil {`, `${indent}\t\t\t\treturn ${zeroOut}, elm_${s}Err`, `${indent}\t\t\t}`);
|
|
2344
|
+
lines.push(emitRowCopy(`el_${s}`, `elm_${s}`, elemRef, plan, indent.length + 3));
|
|
2345
|
+
lines.push(`${indent}\t\t}`);
|
|
2016
2346
|
lines.push(`${indent}\t\tkey_${s} := el_${s}.${dkGo}`);
|
|
2017
2347
|
if (f.drop === "dangling") {
|
|
2018
2348
|
// dangling: a body whose dedupeKey is the zero value (empty string) is dropped.
|
|
@@ -2031,73 +2361,17 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
|
|
|
2031
2361
|
lines.push(c);
|
|
2032
2362
|
return lines.join("\n");
|
|
2033
2363
|
}
|
|
2034
|
-
/**
|
|
2035
|
-
*
|
|
2036
|
-
*
|
|
2037
|
-
* so an OPTIONAL port resolves as `{opt:…}` (native `*T`) and the compiler can see its optionality: a
|
|
2038
|
-
* null-compare becomes a real presence test and a coalesce a real deref-or-default, while a
|
|
2039
|
-
* REQUIRED-scalar slot fed by an opt fails closed. Go values are read directly off the input struct (no
|
|
2040
|
-
* ownership ceremony); a type with no native lowering → null (fail-closed).
|
|
2041
|
-
*/
|
|
2364
|
+
/** #108: resolve an INPUT port head for a native-expr ref (cond `if` / map `when` / branches). A scalar
|
|
2365
|
+
* port → `in.<Field>` of the scalar type; an array port WITH elemType → `in.<Field>` of `[]ElemT` (so
|
|
2366
|
+
* `len(in.<Field>)` works); anything else → null (fail-closed). */
|
|
2042
2367
|
function goInputHeadRef(head, comp, plan) {
|
|
2368
|
+
// #139: the port's declared type comes from the inputPortTypeRef SSoT, so an OPTIONAL port resolves as
|
|
2369
|
+
// {opt:…} (native *T) and the compiler sees its optionality; a type with no native lowering → null.
|
|
2043
2370
|
const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
|
|
2044
2371
|
if (ref === undefined)
|
|
2045
2372
|
return null;
|
|
2046
2373
|
return { expr: `in.${goFieldName(head)}`, ref };
|
|
2047
2374
|
}
|
|
2048
|
-
/**
|
|
2049
|
-
* optPortCompileG — the OPTIONAL-input port lane (go). The rust twin's mirror: a component may
|
|
2050
|
-
* declare an input port `{opt:T}` (PortSchema `required:false`), and the only native representation of
|
|
2051
|
-
* "the value is absent" is `*T`, so such a port never lowers through the required-scalar inference. The
|
|
2052
|
-
* port expression compiles with the SHARED runtime-free native-expr compiler (native-expr.ts — the same
|
|
2053
|
-
* traversal, type-inference and fail-closed discipline rust uses, so the two agree by construction):
|
|
2054
|
-
*
|
|
2055
|
-
* - `opt($.x)` / `$.x` at port position → `*T` (the leaf receives the absent value as null, exactly
|
|
2056
|
-
* as run_behavior's `evalPorts` passes it).
|
|
2057
|
-
* - `coalesce(opt($.x), <literal>)` → the native deref-or-default — a native `T`, no boxed Value.
|
|
2058
|
-
* - `ne(opt($.x), null)` / `eq(…, null)` → the native presence test (`!= nil` / `== nil`).
|
|
2059
|
-
*
|
|
2060
|
-
* Returns null when the port expression reads NO optional input (the caller keeps its existing lowering).
|
|
2061
|
-
* A port that DOES read one but does not compile — or that compiles to a fallible expression, which this
|
|
2062
|
-
* position cannot hoist — FAILS CLOSED loudly rather than degrade to a required type or a zero value.
|
|
2063
|
-
*/
|
|
2064
|
-
function optPortCompileG(node, inputPorts, where, plan) {
|
|
2065
|
-
if (plan === undefined)
|
|
2066
|
-
return null;
|
|
2067
|
-
if (!exprReadsOptionalInput(node, inputPorts))
|
|
2068
|
-
return null;
|
|
2069
|
-
const resolveHead = (head) => goInputHeadRef(head, { inputPorts }, plan);
|
|
2070
|
-
let c;
|
|
2071
|
-
try {
|
|
2072
|
-
// zeroOut is unreachable here: a port expression that needs an error return is rejected below
|
|
2073
|
-
// (stmts.length > 0), so the backend never renders an early-return with it.
|
|
2074
|
-
c = new NativeExprCompiler(makeGoExprBackend("", resolveHead, plan)).compilePort(node);
|
|
2075
|
-
}
|
|
2076
|
-
catch (e) {
|
|
2077
|
-
if (!(e instanceof GeneratorFailure))
|
|
2078
|
-
throw e;
|
|
2079
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} ${JSON.stringify(node)} reads an OPTIONAL input port but does not lower to a native Go value: ${e.message}`);
|
|
2080
|
-
}
|
|
2081
|
-
if (c.stmts.length > 0)
|
|
2082
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} ${JSON.stringify(node)} reads an OPTIONAL input port through a FALLIBLE expression (one that can raise INT_OVERFLOW / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS). Such an expression lowers to STATEMENTS carrying an early error-return, and a port is built inline in the ports-struct literal — a position that cannot host them. (A cond/guard position CAN host them, so the same expression is covered there — the boundary is the POSITION, not the operator.) Use a non-fallible port expression.`);
|
|
2083
|
-
return c;
|
|
2084
|
-
}
|
|
2085
|
-
/** exprReadsOptionalInput — does this port expression reference an OPTIONAL (`required:false`) input
|
|
2086
|
-
* port? Language-neutral scan for `ref`/`refOpt` heads bound to an optional port. Decides ONLY which
|
|
2087
|
-
* lane a port takes; the lane itself then proves the lowering (or fails closed). */
|
|
2088
|
-
function exprReadsOptionalInput(node, inputPorts) {
|
|
2089
|
-
if (node === null || typeof node !== "object")
|
|
2090
|
-
return false;
|
|
2091
|
-
if (Array.isArray(node))
|
|
2092
|
-
return node.some((el) => exprReadsOptionalInput(el, inputPorts));
|
|
2093
|
-
const rec = node;
|
|
2094
|
-
const keys = Object.keys(rec);
|
|
2095
|
-
if (keys.length === 1 && (keys[0] === "ref" || keys[0] === "refOpt")) {
|
|
2096
|
-
const path = rec[keys[0]];
|
|
2097
|
-
return Array.isArray(path) && typeof path[0] === "string" && inputPortIsOptional(inputPorts?.[path[0]]);
|
|
2098
|
-
}
|
|
2099
|
-
return keys.some((k) => exprReadsOptionalInput(rec[k], inputPorts));
|
|
2100
|
-
}
|
|
2101
2375
|
/**
|
|
2102
2376
|
* emitCondArm — the struct-native exec of a covered cond node (#108). Mirrors run_behavior's cond
|
|
2103
2377
|
* lower (behavior.ts:425-429): compile `if` to a native bool, materialize the TAKEN branch to the
|
|
@@ -2337,19 +2611,19 @@ function makeGoExprBackend(zeroOut, resolveHead, plan) {
|
|
|
2337
2611
|
notOp: (a) => `(!${a})`,
|
|
2338
2612
|
structLit: (name, inits) => `${name}{${inits.map((i) => `${goFieldName(i.field)}: ${i.expr}`).join(", ")}}`,
|
|
2339
2613
|
arrLit: (elemTy, items) => `[]${elemTy}{${items.join(", ")}}`,
|
|
2614
|
+
// a projection port of static string literals — bare `[]string{"a", "b"}` (Go string literals are
|
|
2615
|
+
// already allocation-free; no divergence from the general arr path, so no golden churn).
|
|
2616
|
+
staticStrArr: (literals) => ({ expr: `[]string{${literals.map((s) => JSON.stringify(s)).join(", ")}}`, type: "[]string" }),
|
|
2340
2617
|
// an opt (`*T`) NONE literal is `nil` (the inner type is not needed for the untyped nil in Go).
|
|
2341
2618
|
optNone: (_innerTy) => "nil",
|
|
2342
2619
|
// opt (`*T`) null-presence test: present = `(expr != nil)`, absent = `(expr == nil)`.
|
|
2343
2620
|
optIsSome: (expr) => `(${expr} != nil)`,
|
|
2344
2621
|
optIsNone: (expr) => `(${expr} == nil)`,
|
|
2345
|
-
// opt (`*T`) defaulted to a PURE default
|
|
2346
|
-
//
|
|
2347
|
-
// pointer ONCE, deref it when present, else yield the default. The default sits in the else branch,
|
|
2348
|
-
// so it is evaluated only when absent — run_behavior's lazy right side exactly.
|
|
2622
|
+
// opt (`*T`) defaulted to a PURE default — an immediately-invoked func literal: bind the pointer ONCE,
|
|
2623
|
+
// deref when present, else yield the default (evaluated only when absent — run_behavior's lazy right).
|
|
2349
2624
|
optUnwrapOr: (optExpr, defaultExpr, innerTy) => `func() ${innerTy} { if v := ${optExpr}; v != nil { return *v }; return ${defaultExpr} }()`,
|
|
2350
|
-
// opt defaulted to a FALLIBLE default — a statement if/else
|
|
2351
|
-
//
|
|
2352
|
-
// would swallow (it would return from the literal). Only the nil branch evaluates the default.
|
|
2625
|
+
// opt defaulted to a FALLIBLE default — a statement if/else (NOT a func literal): the default's hoisted
|
|
2626
|
+
// `return zeroOut, err` early returns must leave the ENCLOSING fn. Only the nil branch evaluates it.
|
|
2353
2627
|
optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
|
|
2354
2628
|
`var ${temp} ${ty}`,
|
|
2355
2629
|
`if ${bindTemp} := ${optExpr}; ${bindTemp} != nil {`,
|
|
@@ -2513,13 +2787,15 @@ var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name
|
|
|
2513
2787
|
* mismatch Sprintf, `strings` for a concat builder, `sync` for a parallel stage). ExpectedSpecVersions
|
|
2514
2788
|
* stays as a local `var` for provenance, but the old `dslcontracts.SpecVersions` skew `init()` is GONE
|
|
2515
2789
|
* (change #5 — there is no runtime to skew against once the module is runtime-free). */
|
|
2516
|
-
function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath) {
|
|
2790
|
+
function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath, needStrconv = false) {
|
|
2517
2791
|
const sv = ctx.specVersions;
|
|
2518
2792
|
const imports = [];
|
|
2519
2793
|
if (needFmt)
|
|
2520
2794
|
imports.push(`\t"fmt"`);
|
|
2521
2795
|
if (needMath)
|
|
2522
2796
|
imports.push(`\t"math"`);
|
|
2797
|
+
if (needStrconv)
|
|
2798
|
+
imports.push(`\t"strconv"`);
|
|
2523
2799
|
if (needStrings)
|
|
2524
2800
|
imports.push(`\t"strings"`);
|
|
2525
2801
|
if (needSync)
|
|
@@ -2557,6 +2833,9 @@ function emit(ctx) {
|
|
|
2557
2833
|
const handlerIfaces = [];
|
|
2558
2834
|
const elemCopiers = [];
|
|
2559
2835
|
const runnerCodes = [];
|
|
2836
|
+
// the generated strict de-box functions (deduped by named type across all covered components).
|
|
2837
|
+
const decodeFns = [];
|
|
2838
|
+
const decodeEmitted = new Set();
|
|
2560
2839
|
for (const c of native) {
|
|
2561
2840
|
inStructs.push(emitInStruct(c, plan));
|
|
2562
2841
|
const typedNodes = new Map();
|
|
@@ -2582,9 +2861,13 @@ function emit(ctx) {
|
|
|
2582
2861
|
structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes));
|
|
2583
2862
|
}
|
|
2584
2863
|
}
|
|
2864
|
+
// fail closed for a record inside a non-named top (never a silent trust) — before emitting anything.
|
|
2865
|
+
assertDeBoxableResults(c, typedNodes, plan);
|
|
2585
2866
|
// CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
|
|
2586
2867
|
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
2587
2868
|
handlerIfaces.push(emitHandlerInterface(c, typedNodes, plan));
|
|
2869
|
+
// the strict de-box functions for this component's de-box-eligible (named-record) reads.
|
|
2870
|
+
emitNodeDecodeFns(c, typedNodes, plan, decodeEmitted, decodeFns);
|
|
2588
2871
|
// augmented-element copiers for covered map...into nodes (over fields + concrete into row).
|
|
2589
2872
|
const mm = emitMapElemMaterializers(c, typedNodes, plan);
|
|
2590
2873
|
if (mm)
|
|
@@ -2618,7 +2901,9 @@ function emit(ctx) {
|
|
|
2618
2901
|
// #108: `math` is imported iff the native-expr compiler emitted a fallible helper (bcExpr_modF uses
|
|
2619
2902
|
// math.Mod). The helper library itself is baked into the module only when used (goExprUsed.helpers).
|
|
2620
2903
|
const needMath = goExprUsed.helpers;
|
|
2621
|
-
|
|
2904
|
+
// `strconv` is imported iff a generated de-box parses a numeric field (int/float range check).
|
|
2905
|
+
const needStrconv = decodeFns.some((f) => f.includes("strconv."));
|
|
2906
|
+
const header = emitMinimalHeader(ctx, false, needSync, needFmt, needMath, needStrconv);
|
|
2622
2907
|
const banner = `// COMBINED READ layer (bc#90 — the RUNTIME-FREE read de-box; STRUCT-ONLY, FULLY-NATIVE surface). A
|
|
2623
2908
|
// covered read is a strictly-sequential typed componentRef chain (point reads + relationKind:single
|
|
2624
2909
|
// children), emitted ONLY here: each node builds a native ports struct by direct field assignment (every
|
|
@@ -2642,6 +2927,12 @@ function emit(ctx) {
|
|
|
2642
2927
|
? `// CONCRETE per-node handler-result row structs (typed fields = the node's outType; + error signal).\n${rowStructs.filter((r) => r.trim().length > 0).join("\n\n")}`
|
|
2643
2928
|
: undefined,
|
|
2644
2929
|
`// Per-component CONCRETE handler interfaces (one typed Node_* method per covered node).\n${handlerIfaces.join("\n\n")}`,
|
|
2930
|
+
decodeFns.length
|
|
2931
|
+
? `// strict de-box wire seam — the consumer implements WireRow/WireList over its wire payload\n// (variant classification); the generated decodeWire_* owns strictness (required/optional, present/\n// absent, error assembly, fail-closed). Concrete probe structs — no boxed Value on the seam.\n${emitProbeWireTypes()}`
|
|
2932
|
+
: undefined,
|
|
2933
|
+
decodeFns.length
|
|
2934
|
+
? `// Generated strict de-box functions (per named record type; structured Error Value on mismatch).\n${decodeFns.join("\n\n")}`
|
|
2935
|
+
: undefined,
|
|
2645
2936
|
elemCopiers.length
|
|
2646
2937
|
? `// Augmented map-element copiers (over element fields + the concrete into row — pure assignment).\n${elemCopiers.join("\n\n")}`
|
|
2647
2938
|
: undefined,
|
|
@@ -2687,6 +2978,199 @@ export function goTypedNativeObserve(ir, runtimeImport) {
|
|
|
2687
2978
|
// input decoders (TEST glue): generic *Obj -> concrete In_<comp>. Scalar fields read the typed value
|
|
2688
2979
|
// (a `.(T)` here is on the SDK/wire Value in the OBSERVE companion, never in the covered module).
|
|
2689
2980
|
const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
|
|
2981
|
+
// does any covered read de-box a named record — a componentRef record OR a map/fanout element row
|
|
2982
|
+
// (→ the scripted handler returns a WireRow / []WireRow)?
|
|
2983
|
+
const anyDeBox = native.some((c) => {
|
|
2984
|
+
const tn = new Map();
|
|
2985
|
+
for (const n of c.body)
|
|
2986
|
+
if (!isControlGate(n))
|
|
2987
|
+
tn.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
|
|
2988
|
+
return c.body.some((n) => {
|
|
2989
|
+
if ("cond" in n)
|
|
2990
|
+
return false;
|
|
2991
|
+
if ("map" in n || "fanout" in n)
|
|
2992
|
+
return mapFanoutElemDeBoxEligible(n, tn, plan);
|
|
2993
|
+
return deBoxEligible(tn.get(n.id));
|
|
2994
|
+
});
|
|
2995
|
+
});
|
|
2996
|
+
// scripted wire adapter (TEST glue): wraps a scripted bc Value as a WireRow/WireList and classifies each
|
|
2997
|
+
// attribute's native type into a DynamoDB-flavored wire tag (S/N/BOOL/L/M/NULL). A real consumer
|
|
2998
|
+
// implements WireRow over its own AttributeValue map; this exists only so the harness drives the
|
|
2999
|
+
// generated de-box from the same scripted vectors run_behavior uses — so actualWireType observed in a
|
|
3000
|
+
// mismatch is the PRODUCER tag (e.g. "N"), not the collapsed native type.
|
|
3001
|
+
const wireAdapter = anyDeBox
|
|
3002
|
+
? `// ── scripted wire adapter (TEST glue) ──
|
|
3003
|
+
func scriptedWireTag(v ${PKG}.Value) string {
|
|
3004
|
+
switch v.(type) {
|
|
3005
|
+
case nil:
|
|
3006
|
+
return "NULL"
|
|
3007
|
+
case bool:
|
|
3008
|
+
return "BOOL"
|
|
3009
|
+
case int64, float64:
|
|
3010
|
+
return "N"
|
|
3011
|
+
case string:
|
|
3012
|
+
return "S"
|
|
3013
|
+
case []${PKG}.Value:
|
|
3014
|
+
return "L"
|
|
3015
|
+
case *${PKG}.Obj:
|
|
3016
|
+
return "M"
|
|
3017
|
+
default:
|
|
3018
|
+
return "UNKNOWN"
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
func scriptedWireRaw(v ${PKG}.Value) string {
|
|
3023
|
+
switch t := v.(type) {
|
|
3024
|
+
case nil:
|
|
3025
|
+
return "null"
|
|
3026
|
+
case bool:
|
|
3027
|
+
if t {
|
|
3028
|
+
return "true"
|
|
3029
|
+
}
|
|
3030
|
+
return "false"
|
|
3031
|
+
case int64:
|
|
3032
|
+
return strconv.FormatInt(t, 10)
|
|
3033
|
+
case float64:
|
|
3034
|
+
s, _ := ${PKG}.PyFloatRepr(t)
|
|
3035
|
+
return s
|
|
3036
|
+
case string:
|
|
3037
|
+
return t
|
|
3038
|
+
default:
|
|
3039
|
+
return ${PKG}.EncodeValue(v)
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
func probeStringVal(v ${PKG}.Value) StringProbe {
|
|
3044
|
+
if v == nil {
|
|
3045
|
+
return StringProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
|
|
3046
|
+
}
|
|
3047
|
+
if s, ok := v.(string); ok {
|
|
3048
|
+
return StringProbe{Kind: probeGot, Got: s, ActualWireType: "S", Raw: s}
|
|
3049
|
+
}
|
|
3050
|
+
return StringProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
func probeNumberVal(v ${PKG}.Value) NumberProbe {
|
|
3054
|
+
if v == nil {
|
|
3055
|
+
return NumberProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
|
|
3056
|
+
}
|
|
3057
|
+
switch v.(type) {
|
|
3058
|
+
case int64, float64:
|
|
3059
|
+
return NumberProbe{Kind: probeGot, Got: scriptedWireRaw(v), ActualWireType: "N", Raw: scriptedWireRaw(v)}
|
|
3060
|
+
}
|
|
3061
|
+
return NumberProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
func probeBoolVal(v ${PKG}.Value) BoolProbe {
|
|
3065
|
+
if v == nil {
|
|
3066
|
+
return BoolProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
|
|
3067
|
+
}
|
|
3068
|
+
if b, ok := v.(bool); ok {
|
|
3069
|
+
return BoolProbe{Kind: probeGot, Got: b, ActualWireType: "BOOL", Raw: scriptedWireRaw(v)}
|
|
3070
|
+
}
|
|
3071
|
+
return BoolProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
func probeRowVal(v ${PKG}.Value) RowProbe {
|
|
3075
|
+
if v == nil {
|
|
3076
|
+
return RowProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
|
|
3077
|
+
}
|
|
3078
|
+
if o, ok := v.(*${PKG}.Obj); ok {
|
|
3079
|
+
return RowProbe{Kind: probeGot, Got: scriptedWireRow{obj: o}, ActualWireType: "M"}
|
|
3080
|
+
}
|
|
3081
|
+
return RowProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
func probeListVal(v ${PKG}.Value) ListProbe {
|
|
3085
|
+
if v == nil {
|
|
3086
|
+
return ListProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
|
|
3087
|
+
}
|
|
3088
|
+
if a, ok := v.([]${PKG}.Value); ok {
|
|
3089
|
+
return ListProbe{Kind: probeGot, Got: scriptedWireList{items: a}, ActualWireType: "L"}
|
|
3090
|
+
}
|
|
3091
|
+
return ListProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
type scriptedWireRow struct {
|
|
3095
|
+
obj *${PKG}.Obj
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
func newScriptedWireRow(v ${PKG}.Value) WireRow {
|
|
3099
|
+
o, _ := v.(*${PKG}.Obj)
|
|
3100
|
+
return scriptedWireRow{obj: o}
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
func (w scriptedWireRow) field(name string) (${PKG}.Value, bool) {
|
|
3104
|
+
if w.obj == nil {
|
|
3105
|
+
return nil, false
|
|
3106
|
+
}
|
|
3107
|
+
return w.obj.Get(name)
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
func (w scriptedWireRow) Keys() []string {
|
|
3111
|
+
if w.obj == nil {
|
|
3112
|
+
return nil
|
|
3113
|
+
}
|
|
3114
|
+
return w.obj.Keys
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
func (w scriptedWireRow) ProbeString(field string) StringProbe {
|
|
3118
|
+
if v, ok := w.field(field); ok {
|
|
3119
|
+
return probeStringVal(v)
|
|
3120
|
+
}
|
|
3121
|
+
return StringProbe{Kind: probeAbsent}
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
func (w scriptedWireRow) ProbeNumber(field string) NumberProbe {
|
|
3125
|
+
if v, ok := w.field(field); ok {
|
|
3126
|
+
return probeNumberVal(v)
|
|
3127
|
+
}
|
|
3128
|
+
return NumberProbe{Kind: probeAbsent}
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
func (w scriptedWireRow) ProbeBool(field string) BoolProbe {
|
|
3132
|
+
if v, ok := w.field(field); ok {
|
|
3133
|
+
return probeBoolVal(v)
|
|
3134
|
+
}
|
|
3135
|
+
return BoolProbe{Kind: probeAbsent}
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
func (w scriptedWireRow) ProbeRow(field string) RowProbe {
|
|
3139
|
+
if v, ok := w.field(field); ok {
|
|
3140
|
+
return probeRowVal(v)
|
|
3141
|
+
}
|
|
3142
|
+
return RowProbe{Kind: probeAbsent}
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
func (w scriptedWireRow) ProbeList(field string) ListProbe {
|
|
3146
|
+
if v, ok := w.field(field); ok {
|
|
3147
|
+
return probeListVal(v)
|
|
3148
|
+
}
|
|
3149
|
+
return ListProbe{Kind: probeAbsent}
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
type scriptedWireList struct {
|
|
3153
|
+
items []${PKG}.Value
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
func (l scriptedWireList) Len() int { return len(l.items) }
|
|
3157
|
+
func (l scriptedWireList) ElemString(i int) StringProbe { return probeStringVal(l.items[i]) }
|
|
3158
|
+
func (l scriptedWireList) ElemNumber(i int) NumberProbe { return probeNumberVal(l.items[i]) }
|
|
3159
|
+
func (l scriptedWireList) ElemBool(i int) BoolProbe { return probeBoolVal(l.items[i]) }
|
|
3160
|
+
func (l scriptedWireList) ElemRow(i int) RowProbe { return probeRowVal(l.items[i]) }
|
|
3161
|
+
func (l scriptedWireList) ElemList(i int) ListProbe { return probeListVal(l.items[i]) }
|
|
3162
|
+
|
|
3163
|
+
// FailureDetail exposes the structured Error Value fields WITHOUT naming the local ErrorDetail type, so
|
|
3164
|
+
// the harness can assert the de-box's kind / field / expectedType / actualWireType / rawValue (TEST
|
|
3165
|
+
// glue — same package as the covered module, so it may read the fields directly).
|
|
3166
|
+
func (e *BehaviorError) FailureDetail() (kind, model, field, expectedType, actualWireType, rawValue string, ok bool) {
|
|
3167
|
+
if e.Detail == nil {
|
|
3168
|
+
return "", "", "", "", "", "", false
|
|
3169
|
+
}
|
|
3170
|
+
d := e.Detail
|
|
3171
|
+
return d.Kind, d.Model, d.Field, d.ExpectedType, d.ActualWireType, d.RawValue, true
|
|
3172
|
+
}`
|
|
3173
|
+
: "";
|
|
2690
3174
|
// The observe companion drives EVERY covered component, so its handler type param H must satisfy
|
|
2691
3175
|
// EVERY per-component concrete Handler_<comp> interface — a combined constraint embeds them all.
|
|
2692
3176
|
const combinedConstraint = `allNativeRawHandlers`;
|
|
@@ -2712,35 +3196,56 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
|
2712
3196
|
continue; // #108: cond has no handler method (pure Expression) — no scripted impl.
|
|
2713
3197
|
const ref = typedNodes.get(n.id);
|
|
2714
3198
|
if ("fanout" in n) {
|
|
2715
|
-
// v3: scripted fanout impl — drain one batched outcome and decode the aligned []Value into the
|
|
2716
|
-
// per-body elem rows (a null array element materializes to the zero elem row = dangling).
|
|
2717
3199
|
const f = n.fanout;
|
|
2718
|
-
const
|
|
3200
|
+
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
2719
3201
|
const elemT = rawElemStructName(c.name, n.id);
|
|
2720
3202
|
const batchT = rawRowStructName(c.name, n.id);
|
|
2721
|
-
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2722
3203
|
const portsT = `${portsStructName(c.name, n.id)}_batch`;
|
|
2723
3204
|
const foBoundT = boundGoType(n, c, typedNodes, plan);
|
|
2724
|
-
|
|
3205
|
+
if (elemDeBox) {
|
|
3206
|
+
// a record-element fanout returns the aligned element WIRES; the covered runner de-boxes each.
|
|
3207
|
+
// A null aligned body is a DANGLING id — a nil wire (the runner maps it to the zero elem row).
|
|
3208
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${foBoundT}) ([]WireRow, error) {
|
|
3209
|
+
src, ok := s.next(${JSON.stringify(f.component)})
|
|
3210
|
+
if !ok {
|
|
3211
|
+
return nil, unknownComponent(${JSON.stringify(f.component)})
|
|
3212
|
+
}
|
|
3213
|
+
if src.isErr {
|
|
3214
|
+
return nil, leafFailure(src.err)
|
|
3215
|
+
}
|
|
3216
|
+
arr, _ := src.ok.([]${PKG}.Value)
|
|
3217
|
+
wires := make([]WireRow, 0, len(arr))
|
|
3218
|
+
for _, ev := range arr {
|
|
3219
|
+
if ev == nil {
|
|
3220
|
+
wires = append(wires, nil)
|
|
3221
|
+
continue
|
|
3222
|
+
}
|
|
3223
|
+
wires = append(wires, newScriptedWireRow(ev))
|
|
3224
|
+
}
|
|
3225
|
+
return wires, nil
|
|
3226
|
+
}`);
|
|
3227
|
+
continue;
|
|
3228
|
+
}
|
|
3229
|
+
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
3230
|
+
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
3231
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${foBoundT}) (${batchT}, error) {
|
|
2725
3232
|
src, ok := s.next(${JSON.stringify(f.component)})
|
|
2726
3233
|
if !ok {
|
|
2727
|
-
return ${batchT}{},
|
|
3234
|
+
return ${batchT}{}, unknownComponent(${JSON.stringify(f.component)})
|
|
2728
3235
|
}
|
|
2729
3236
|
if src.isErr {
|
|
2730
|
-
return ${batchT}{
|
|
3237
|
+
return ${batchT}{}, leafFailure(src.err)
|
|
2731
3238
|
}
|
|
2732
3239
|
arr, _ := src.ok.([]${PKG}.Value)
|
|
2733
3240
|
rows := make([]${elemT}, 0, len(arr))
|
|
2734
3241
|
for _, ev := range arr {
|
|
2735
|
-
// v3: a null aligned body is a DANGLING id — decode it to the zero elem row (empty dedupeKey
|
|
2736
|
-
// field), so the runner's dangling-drop excludes it (byte-equal to fanoutDedupDrop).
|
|
2737
3242
|
if ev == nil {
|
|
2738
3243
|
rows = append(rows, ${elemT}{})
|
|
2739
3244
|
continue
|
|
2740
3245
|
}
|
|
2741
3246
|
rows = append(rows, ${elemDecode}(ev))
|
|
2742
3247
|
}
|
|
2743
|
-
return ${batchT}{Rows: rows},
|
|
3248
|
+
return ${batchT}{Rows: rows}, nil
|
|
2744
3249
|
}`);
|
|
2745
3250
|
continue;
|
|
2746
3251
|
}
|
|
@@ -2748,72 +3253,172 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
|
2748
3253
|
const m = n.map;
|
|
2749
3254
|
const batched = m.batched === true;
|
|
2750
3255
|
const arrRef = ref;
|
|
2751
|
-
const
|
|
3256
|
+
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
2752
3257
|
const elemT = rawElemStructName(c.name, n.id);
|
|
2753
3258
|
const batchT = rawRowStructName(c.name, n.id);
|
|
2754
|
-
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2755
3259
|
const portsT = batched ? `${portsStructName(c.name, n.id)}_batch` : portsStructName(c.name, n.id);
|
|
2756
|
-
const retT = batched ? batchT : elemT;
|
|
2757
3260
|
const mapBoundT = boundGoType(n, c, typedNodes, plan);
|
|
3261
|
+
if (elemDeBox) {
|
|
3262
|
+
// a record-element map returns the element WIRE(s); the covered runner de-boxes each strictly.
|
|
3263
|
+
if (batched) {
|
|
3264
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) ([]WireRow, error) {
|
|
3265
|
+
src, ok := s.next(${JSON.stringify(m.component)})
|
|
3266
|
+
if !ok {
|
|
3267
|
+
return nil, unknownComponent(${JSON.stringify(m.component)})
|
|
3268
|
+
}
|
|
3269
|
+
if src.isErr {
|
|
3270
|
+
return nil, leafFailure(src.err)
|
|
3271
|
+
}
|
|
3272
|
+
arr, _ := src.ok.([]${PKG}.Value)
|
|
3273
|
+
wires := make([]WireRow, 0, len(arr))
|
|
3274
|
+
for _, ev := range arr {
|
|
3275
|
+
wires = append(wires, newScriptedWireRow(ev))
|
|
3276
|
+
}
|
|
3277
|
+
return wires, nil
|
|
3278
|
+
}`);
|
|
3279
|
+
}
|
|
3280
|
+
else {
|
|
3281
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (WireRow, error) {
|
|
3282
|
+
src, ok := s.next(${JSON.stringify(m.component)})
|
|
3283
|
+
if !ok {
|
|
3284
|
+
return nil, unknownComponent(${JSON.stringify(m.component)})
|
|
3285
|
+
}
|
|
3286
|
+
if src.isErr {
|
|
3287
|
+
return nil, leafFailure(src.err)
|
|
3288
|
+
}
|
|
3289
|
+
return newScriptedWireRow(src.ok), nil
|
|
3290
|
+
}`);
|
|
3291
|
+
}
|
|
3292
|
+
continue;
|
|
3293
|
+
}
|
|
3294
|
+
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
3295
|
+
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2758
3296
|
if (batched) {
|
|
2759
|
-
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${batchT},
|
|
3297
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${batchT}, error) {
|
|
2760
3298
|
src, ok := s.next(${JSON.stringify(m.component)})
|
|
2761
3299
|
if !ok {
|
|
2762
|
-
return ${batchT}{},
|
|
3300
|
+
return ${batchT}{}, unknownComponent(${JSON.stringify(m.component)})
|
|
2763
3301
|
}
|
|
2764
3302
|
if src.isErr {
|
|
2765
|
-
return ${batchT}{
|
|
3303
|
+
return ${batchT}{}, leafFailure(src.err)
|
|
2766
3304
|
}
|
|
2767
3305
|
arr, _ := src.ok.([]${PKG}.Value)
|
|
2768
3306
|
rows := make([]${elemT}, 0, len(arr))
|
|
2769
3307
|
for _, ev := range arr {
|
|
2770
3308
|
rows = append(rows, ${elemDecode}(ev))
|
|
2771
3309
|
}
|
|
2772
|
-
return ${batchT}{Rows: rows},
|
|
3310
|
+
return ${batchT}{Rows: rows}, nil
|
|
2773
3311
|
}`);
|
|
2774
3312
|
}
|
|
2775
3313
|
else {
|
|
2776
|
-
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${elemT},
|
|
3314
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${elemT}, error) {
|
|
2777
3315
|
src, ok := s.next(${JSON.stringify(m.component)})
|
|
2778
3316
|
if !ok {
|
|
2779
|
-
return ${elemT}{},
|
|
3317
|
+
return ${elemT}{}, unknownComponent(${JSON.stringify(m.component)})
|
|
2780
3318
|
}
|
|
2781
3319
|
if src.isErr {
|
|
2782
|
-
return ${elemT}{
|
|
3320
|
+
return ${elemT}{}, leafFailure(src.err)
|
|
2783
3321
|
}
|
|
2784
|
-
return ${elemDecode}(src.ok),
|
|
3322
|
+
return ${elemDecode}(src.ok), nil
|
|
2785
3323
|
}`);
|
|
2786
3324
|
}
|
|
2787
|
-
void retT;
|
|
2788
3325
|
continue;
|
|
2789
3326
|
}
|
|
2790
3327
|
const node = n;
|
|
2791
3328
|
const rowT = rawRowStructName(c.name, node.id);
|
|
2792
|
-
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
2793
3329
|
const nodeBoundT = boundGoType(node, c, typedNodes, plan);
|
|
2794
|
-
//
|
|
2795
|
-
//
|
|
2796
|
-
//
|
|
2797
|
-
//
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
3330
|
+
// a de-box-eligible node's scripted handler returns a scriptedWireRow (the scripted Value
|
|
3331
|
+
// wrapped as a WireRow that classifies each attribute's native type into a DynamoDB-flavored wire
|
|
3332
|
+
// tag); the covered module's generated decodeWire_* then probes it. This mirrors a real consumer,
|
|
3333
|
+
// whose handler returns its wire payload for BC to de-box — the de-box no longer lives here.
|
|
3334
|
+
if (deBoxEligible(ref)) {
|
|
3335
|
+
// a record-containing top returns its wire (WireRow / []WireRow / nilable WireRow /
|
|
3336
|
+
// map[string]WireRow) — the covered runner de-boxes each record. #129: an arr node with an `items`
|
|
3337
|
+
// port echoes the NATIVE port array; it is serialized back to []WireRow so the de-box round-trips.
|
|
3338
|
+
const top = recordTop(ref);
|
|
3339
|
+
const wireT = goWireTypeForTop(top);
|
|
3340
|
+
// echo-into-record (#141 fixtures): a named de-box node whose result WRAPS an input port — the
|
|
3341
|
+
// scripted leaf echoes port P into the record's single field, so the input-port value flows into
|
|
3342
|
+
// the de-boxed result and the runtime non-vacuity #139/#143/#144 assert survives the record wrap.
|
|
3343
|
+
let namedEcho = "";
|
|
3344
|
+
if (top.kind === "named") {
|
|
3345
|
+
const decl = findDecl(plan, top.innerName);
|
|
3346
|
+
const recordGo = renderTypeRef(ref);
|
|
3347
|
+
// a SINGLE-field record admits a scalar-into-field wrap (echo:port + wrap:"v" → {v: ports.X}).
|
|
3348
|
+
const field = decl !== undefined && decl.fields.length === 1 ? decl.fields[0] : undefined;
|
|
3349
|
+
const fieldGo = field !== undefined ? renderTypeRef(field.type) : null;
|
|
3350
|
+
for (const pn of Object.keys(node.ports)) {
|
|
3351
|
+
let portGo = null;
|
|
3352
|
+
try {
|
|
3353
|
+
portGo = portFieldGoType(node.ports[pn], c, plan, typedNodes, `echo port '${pn}'`);
|
|
3354
|
+
}
|
|
3355
|
+
catch {
|
|
3356
|
+
portGo = null;
|
|
3357
|
+
}
|
|
3358
|
+
if (portGo === recordGo) {
|
|
3359
|
+
// case 2: the port IS the whole record (its type == the node's record type) → echo it
|
|
3360
|
+
// DIRECTLY (no field wrap), matching run_behavior's plain echo:port of a whole-record port.
|
|
3361
|
+
namedEcho += `\tif src.echo == ${JSON.stringify(pn)} {\n\t\treturn newScriptedWireRow(${serializeTyped(`ports.${goPortFieldName(pn)}`, ref, plan)}), nil\n\t}\n`;
|
|
3362
|
+
}
|
|
3363
|
+
else if (fieldGo !== null && field !== undefined && portGo === fieldGo) {
|
|
3364
|
+
// case 1: a scalar port wraps into the record's single field ({v: ports.X}).
|
|
3365
|
+
namedEcho += `\tif src.echo == ${JSON.stringify(pn)} {\n\t\treturn newScriptedWireRow(${serializeTyped(`${recordGo}{${goFieldName(field.name)}: ports.${goPortFieldName(pn)}}`, ref, plan)}), nil\n\t}\n`;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
const canEcho = (top.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items")) || namedEcho !== "";
|
|
3370
|
+
const portsParam = canEcho ? "ports" : "_";
|
|
3371
|
+
let body;
|
|
3372
|
+
if (top.kind === "named") {
|
|
3373
|
+
body = `${namedEcho}\treturn newScriptedWireRow(src.ok), nil`;
|
|
3374
|
+
}
|
|
3375
|
+
else if (top.kind === "arr") {
|
|
3376
|
+
const elemRef = ref.elem;
|
|
3377
|
+
const itemsF = goPortFieldName("items");
|
|
3378
|
+
const echoBranch = canEcho
|
|
3379
|
+
? `\tif src.echo == "items" {\n\t\twires := make([]WireRow, 0, len(ports.${itemsF}))\n\t\tfor i := range ports.${itemsF} {\n\t\t\twires = append(wires, newScriptedWireRow(${serializeTyped(`ports.${itemsF}[i]`, elemRef, plan)}))\n\t\t}\n\t\treturn wires, nil\n\t}\n`
|
|
3380
|
+
: "";
|
|
3381
|
+
body = `${echoBranch}\tarr, _ := src.ok.([]${PKG}.Value)\n\twires := make([]WireRow, 0, len(arr))\n\tfor _, ev := range arr {\n\t\twires = append(wires, newScriptedWireRow(ev))\n\t}\n\treturn wires, nil`;
|
|
3382
|
+
}
|
|
3383
|
+
else if (top.kind === "opt") {
|
|
3384
|
+
body = `\tif src.ok == nil {\n\t\treturn nil, nil\n\t}\n\treturn newScriptedWireRow(src.ok), nil`;
|
|
3385
|
+
}
|
|
3386
|
+
else {
|
|
3387
|
+
body = `\tobj, _ := src.ok.(*${PKG}.Obj)\n\twires := make(map[string]WireRow)\n\tif obj != nil {\n\t\tfor _, k := range obj.Keys {\n\t\t\twires[k] = newScriptedWireRow(obj.Vals[k])\n\t\t}\n\t}\n\treturn wires, nil`;
|
|
3388
|
+
}
|
|
3389
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, node.id)}(${portsParam} ${portsStructName(c.name, node.id)}, _ ${nodeBoundT}) (${wireT}, error) {
|
|
2809
3390
|
src, ok := s.next(${JSON.stringify(node.component)})
|
|
2810
3391
|
if !ok {
|
|
2811
|
-
return ${
|
|
3392
|
+
return nil, unknownComponent(${JSON.stringify(node.component)})
|
|
2812
3393
|
}
|
|
2813
3394
|
if src.isErr {
|
|
2814
|
-
return
|
|
3395
|
+
return nil, leafFailure(src.err)
|
|
2815
3396
|
}
|
|
2816
|
-
${
|
|
3397
|
+
${body}
|
|
3398
|
+
}`);
|
|
3399
|
+
continue;
|
|
3400
|
+
}
|
|
3401
|
+
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
3402
|
+
// #129 (TEST glue): if this node has an `items` port and its outType is a bare arr, support the
|
|
3403
|
+
// reference scriptedHandlers' `echo === "items"` — the scripted handler ECHOES the NATIVE `items`
|
|
3404
|
+
// port back as its result. This makes the node→leaf array dataflow test non-vacuous: the leaf's
|
|
3405
|
+
// observed result is exactly the native array that flowed into its ports struct, so a dropped or
|
|
3406
|
+
// boxed native array would diverge from run_behavior. `ports` is named only when the echo path
|
|
3407
|
+
// uses it (else the param stays `_` so a non-echo node compiles clean / unused-param-free).
|
|
3408
|
+
const canEcho = ref.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items");
|
|
3409
|
+
const portsParam = canEcho ? "ports" : "_";
|
|
3410
|
+
const echoBranch = canEcho
|
|
3411
|
+
? `\tif src.echo == "items" {\n\t\treturn ${rowT}{Val: ports.${goPortFieldName("items")}}, nil\n\t}\n`
|
|
3412
|
+
: "";
|
|
3413
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, node.id)}(${portsParam} ${portsStructName(c.name, node.id)}, _ ${nodeBoundT}) (${rowT}, error) {
|
|
3414
|
+
src, ok := s.next(${JSON.stringify(node.component)})
|
|
3415
|
+
if !ok {
|
|
3416
|
+
return ${rowT}{}, unknownComponent(${JSON.stringify(node.component)})
|
|
3417
|
+
}
|
|
3418
|
+
if src.isErr {
|
|
3419
|
+
return ${rowT}{}, leafFailure(src.err)
|
|
3420
|
+
}
|
|
3421
|
+
${echoBranch} return ${decode}(src.ok), nil
|
|
2817
3422
|
}`);
|
|
2818
3423
|
}
|
|
2819
3424
|
}
|
|
@@ -2856,15 +3461,13 @@ func NewScriptedNativeRaw(spec *${PKG}.JObj) (*ScriptedNativeRaw, error) {
|
|
|
2856
3461
|
for _, e := range entries {
|
|
2857
3462
|
eo, _ := e.(*${PKG}.JObj)
|
|
2858
3463
|
// #129: an \`echo\` directive names an input port to echo back (mirrors the reference
|
|
2859
|
-
// scriptedHandlers'
|
|
2860
|
-
// payload. \`{"echo":"port","port":"X"}\` is normalized to the port NAME here, so the
|
|
2861
|
-
// generated node method just compares \`src.echo\` against its echoable port names —
|
|
2862
|
-
// \`{"echo":"items"}\` is already that shape.
|
|
3464
|
+
// scriptedHandlers' \`echo === "items"\`). It is drained together with the ok payload.
|
|
2863
3465
|
echo := ""
|
|
2864
3466
|
if echoNode, has := eo.Get("echo"); has {
|
|
2865
3467
|
echo, _ = echoNode.(string)
|
|
3468
|
+
// normalize echo:port to the port NAME X so a de-box handler's echo comparison fires;
|
|
3469
|
+
// echo:items already names the port directly.
|
|
2866
3470
|
if echo == "port" {
|
|
2867
|
-
echo = ""
|
|
2868
3471
|
if portNode, hasPort := eo.Get("port"); hasPort {
|
|
2869
3472
|
echo, _ = portNode.(string)
|
|
2870
3473
|
}
|
|
@@ -2914,11 +3517,14 @@ func (s *ScriptedNativeRaw) next(component string) (scriptSrc, bool) {
|
|
|
2914
3517
|
package behaviors
|
|
2915
3518
|
|
|
2916
3519
|
import (
|
|
2917
|
-
"sync"
|
|
3520
|
+
${anyDeBox ? '\t"strconv"\n\n' : ""} "sync"
|
|
2918
3521
|
|
|
2919
3522
|
${PKG} "${runtimeImport ?? "github.com/foo-ogawa/behavior-contracts/go"}"
|
|
2920
3523
|
)
|
|
2921
3524
|
|
|
3525
|
+
// scripted wire adapter (TEST glue) — wraps a scripted Value as a WireRow the generated de-box probes.
|
|
3526
|
+
${wireAdapter}
|
|
3527
|
+
|
|
2922
3528
|
// typed -> Value serializers (TEST glue only — the covered module has none).
|
|
2923
3529
|
${serializers}
|
|
2924
3530
|
|
|
@@ -2952,16 +3558,6 @@ ${arms}
|
|
|
2952
3558
|
}
|
|
2953
3559
|
`;
|
|
2954
3560
|
}
|
|
2955
|
-
/** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
|
|
2956
|
-
* declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row's Val. A port
|
|
2957
|
-
* with a different type is not echoable (the row could not hold it), and a NAMED outType has no Val at
|
|
2958
|
-
* all (its row carries the struct's fields flattened — see emitDecodeRow). */
|
|
2959
|
-
function echoablePorts(comp, node, ref, plan, typedNodes) {
|
|
2960
|
-
if (ref.kind === "named")
|
|
2961
|
-
return [];
|
|
2962
|
-
const want = renderTypeRef(ref);
|
|
2963
|
-
return Object.keys(node.ports).filter((p) => portFieldGoType(node.ports[p], comp.inputPorts, `port '${p}'`, undefined, plan, typedNodes) === want);
|
|
2964
|
-
}
|
|
2965
3561
|
/** emitDecodeRow — emit (once, deduped) a decoder `decodeRow_<comp>_<node><suffix>(v Value) <rowT>`
|
|
2966
3562
|
* that materializes the scripted ok Value into the concrete row struct (typed fields = the outType /
|
|
2967
3563
|
* elem-row type). Named: reuse the Value->struct marshaller + copy fields; scalar/arr/opt: materialize
|