behavior-contracts 0.8.5 → 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 +1332 -261
- 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 +1328 -352
- package/dist/generator/emit-typed-native-rust.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,21 +614,14 @@ 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
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* field: every port lowers to a concrete Go type, and a port that cannot lower FAILS CLOSED. The field
|
|
56
|
-
* type is the compiled expression's TypeRef, so it can never disagree with the initializer (emitPortInit),
|
|
57
|
-
* which is the SAME lowering read for its value. `where` names the emission site for the error.
|
|
58
|
-
*/
|
|
59
|
-
function portFieldGoType(node, comp, plan, typedNodes, where = "port", opts) {
|
|
60
|
-
const c = compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), where, opts);
|
|
61
|
-
return c.renderedType ?? renderTypeRef(c.ref);
|
|
617
|
+
/** scalar TypeRef → PortFieldType (native go scalar kind). */
|
|
618
|
+
function goScalarPortField(scalar) {
|
|
619
|
+
return scalar === "string" ? "string" : scalar === "int" ? "int64" : scalar === "float" ? "float64" : "bool";
|
|
62
620
|
}
|
|
63
621
|
/**
|
|
64
622
|
* goNativeResolveHead — the ONE ref-head resolver the runtime-free native-expr compiler reads, for cond
|
|
65
|
-
* `if`/branches, a map/fanout `when` guard, AND a port
|
|
66
|
-
*
|
|
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.
|
|
67
625
|
*/
|
|
68
626
|
function goNativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
|
|
69
627
|
return (head) => {
|
|
@@ -78,18 +636,14 @@ function goNativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
|
|
|
78
636
|
/**
|
|
79
637
|
* compilePortNode — lower a port expression to its native ports-struct field via the runtime-free
|
|
80
638
|
* native-expr compiler (native-expr.ts): the ONE port lowering. The port's field TYPE is the compiled
|
|
81
|
-
* `ref`, its initializer the compiled `expr` —
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* returned here as-is; only the value site (emitPortInit) rejects it, since a ports-struct literal cannot
|
|
86
|
-
* host statements — the coverage gate/type site must still see it as covered.
|
|
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.
|
|
87
643
|
*/
|
|
88
644
|
function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
|
|
89
645
|
const resolveHead = goNativeResolveHead(comp, plan, typedNodes, isPrior, opts);
|
|
90
646
|
try {
|
|
91
|
-
// zeroOut is unreachable: a fallible port is rejected at emitPortInit (a ports-struct literal hosts no
|
|
92
|
-
// statements), so the backend never renders an early-return with it.
|
|
93
647
|
return new NativeExprCompiler(makeGoExprBackend("", resolveHead, plan)).compilePort(node);
|
|
94
648
|
}
|
|
95
649
|
catch (e) {
|
|
@@ -98,8 +652,73 @@ function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
|
|
|
98
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}`);
|
|
99
653
|
}
|
|
100
654
|
}
|
|
101
|
-
/**
|
|
102
|
-
*
|
|
655
|
+
/**
|
|
656
|
+
* portFieldGoType — the NATIVE Go type for a covered port field (bc#90 / runtime-free). The covered
|
|
657
|
+
* ports struct carries NO `dslcontracts.Value` field: every port lowers to a concrete Go type.
|
|
658
|
+
* - a static string-array (projection) → `[]string` (change #2);
|
|
659
|
+
* - a bare int/float number literal (e.g. Query `limit: 20`) → `int64`/`float64`;
|
|
660
|
+
* - a string/bool/scalar-input port → the scalar Go type (inferPortType).
|
|
661
|
+
* A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
|
|
662
|
+
* Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no
|
|
663
|
+
* `dslcontracts.Value` fallback on the native module). `where` names the emission site for the error.
|
|
664
|
+
*/
|
|
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);
|
|
668
|
+
}
|
|
669
|
+
function staticArrElems(node) {
|
|
670
|
+
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
671
|
+
return null;
|
|
672
|
+
const keys = Object.keys(node);
|
|
673
|
+
if (keys.length !== 1 || keys[0] !== "arr")
|
|
674
|
+
return null;
|
|
675
|
+
const arg = node.arr;
|
|
676
|
+
return Array.isArray(arg) ? arg : null;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* isNativeRawComponent — a component covered by the combined read emitter. Strictly sequential
|
|
680
|
+
* (no real concurrency), body is ALL componentRef (no map/cond), default 'fail'/'continue'
|
|
681
|
+
* policy, relationKind is single/connection or absent, and EVERY node carries outType + the
|
|
682
|
+
* component carries outputType. relationKind:single / bindField ARE the point — inline exec makes
|
|
683
|
+
* the child-present decision from the real parent result. Ports AND the output are evaluated via
|
|
684
|
+
* the primitive SSoT over a Value-space portScope (so a dynamic operator in a port or an
|
|
685
|
+
* obj-assembly output is fine — the SSoT handles fail-closed semantics; only the RESULT plane +
|
|
686
|
+
* PORT container go native). A covered SHAPE that lacks outType is a fail-closed error raised at
|
|
687
|
+
* emit (see assertTypedCoverage) — never a silent re-box.
|
|
688
|
+
*/
|
|
689
|
+
/**
|
|
690
|
+
* numLiteralGoExpr — a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
|
|
691
|
+
* `classifyExpr` deliberately leaves a bare number `dynamic` (its checked int/float range rules are
|
|
692
|
+
* the evaluate SSoT, expr.go evalNumberLiteral), so a plain number never reaches the str/bool/null
|
|
693
|
+
* static branch. But a number literal IS statically resolvable to a native Go Value: the SAME
|
|
694
|
+
* range check the runtime applies is performed HERE at generation time (integral literal in the
|
|
695
|
+
* safe 2^53-1 window → int64; a fractional/exponent literal → the checked float64). Returns the Go
|
|
696
|
+
* expression for the literal's Value, or null when the node is not a bare number OR the integral
|
|
697
|
+
* literal is out of the safe window (that out-of-range case keeps the runtime INVALID_LITERAL
|
|
698
|
+
* semantics — the port is NOT covered natively, so the whole component falls through, never a
|
|
699
|
+
* silently-wrong inline). Mirrors coderuntime.evalNumberLiteral exactly.
|
|
700
|
+
*/
|
|
701
|
+
const NUM_SAFE_INT = 9007199254740991; // 2^53 - 1 (expr.go safeInt)
|
|
702
|
+
function numLiteralGoExpr(node) {
|
|
703
|
+
if (typeof node !== "number" || !Number.isFinite(node))
|
|
704
|
+
return null;
|
|
705
|
+
if (Number.isInteger(node)) {
|
|
706
|
+
if (node < -NUM_SAFE_INT || node > NUM_SAFE_INT)
|
|
707
|
+
return null; // out of safe window → not covered (runtime INVALID_LITERAL)
|
|
708
|
+
return `${PKG}.Value(int64(${node}))`;
|
|
709
|
+
}
|
|
710
|
+
// fractional / exponent literal → checked float64 (finite, already guaranteed above).
|
|
711
|
+
return `${PKG}.Value(float64(${node}))`;
|
|
712
|
+
}
|
|
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). */
|
|
103
722
|
function buildTypedNodes(comp, plan) {
|
|
104
723
|
const typedNodes = new Map();
|
|
105
724
|
for (const n of comp.body) {
|
|
@@ -114,25 +733,26 @@ function buildTypedNodes(comp, plan) {
|
|
|
114
733
|
}
|
|
115
734
|
return typedNodes;
|
|
116
735
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
}
|
|
125
745
|
}
|
|
126
|
-
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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. */
|
|
129
749
|
function portIsStatic(node, comp, plan, typedNodes, asBinding) {
|
|
130
750
|
try {
|
|
131
751
|
compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), "port", asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined);
|
|
132
752
|
return true;
|
|
133
753
|
}
|
|
134
754
|
catch {
|
|
135
|
-
return false;
|
|
755
|
+
return false;
|
|
136
756
|
}
|
|
137
757
|
}
|
|
138
758
|
/** The output must lower to a typed struct/value with no Value-scope read: a ref whose head is a
|
|
@@ -180,8 +800,8 @@ function isFullyTyped(comp) {
|
|
|
180
800
|
}
|
|
181
801
|
return true;
|
|
182
802
|
}
|
|
183
|
-
function isNativeRaw(comp
|
|
184
|
-
return isSequentialComponentRefRead(comp
|
|
803
|
+
function isNativeRaw(comp) {
|
|
804
|
+
return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
|
|
185
805
|
}
|
|
186
806
|
/**
|
|
187
807
|
* coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet). A BATCHED `map` node whose:
|
|
@@ -196,7 +816,7 @@ function isNativeRaw(comp, plan) {
|
|
|
196
816
|
* element struct by copying the over element's typed fields + materializing the into field from the
|
|
197
817
|
* aligned batch raw — NO boxed Value on any plane.
|
|
198
818
|
*/
|
|
199
|
-
function coveredMapNode(n, bodyIds, comp
|
|
819
|
+
function coveredMapNode(n, bodyIds, comp) {
|
|
200
820
|
if (n === null || typeof n !== "object" || !("map" in n))
|
|
201
821
|
return false;
|
|
202
822
|
const m = n.map;
|
|
@@ -213,7 +833,7 @@ function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
|
|
|
213
833
|
// A map node must carry an outType (its ELEMENT type per the recorder SoT — authoring.ts recordMap
|
|
214
834
|
// holds `node.outType = mapElem`, the PRE-array-wrap element; the emitter synthesizes the produced
|
|
215
835
|
// `[]element`). A map WITHOUT outType is uncovered. Legacy hand-built IR that already carries the
|
|
216
|
-
//
|
|
836
|
+
// The map node's outType is the ELEMENT type; mapNodeArrRef wraps it to the produced array ref.
|
|
217
837
|
const outT = n.outType;
|
|
218
838
|
if (outT === undefined || typeof outT !== "object")
|
|
219
839
|
return false;
|
|
@@ -233,22 +853,14 @@ function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
|
|
|
233
853
|
return false;
|
|
234
854
|
}
|
|
235
855
|
const ports = m.ports;
|
|
856
|
+
const plan = planForComp(comp);
|
|
857
|
+
const typedNodes = buildTypedNodes(comp, plan);
|
|
236
858
|
const asBinding = mapAsBinding(comp, n, typedNodes, plan);
|
|
237
859
|
for (const p of Object.keys(ports))
|
|
238
860
|
if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
|
|
239
861
|
return false;
|
|
240
862
|
return true;
|
|
241
863
|
}
|
|
242
|
-
/** the `$as` element binding for a covered map/fanout node's element ports (name + over-element TypeRef),
|
|
243
|
-
* or undefined when the over element type does not resolve (the node is then uncovered upstream). */
|
|
244
|
-
function mapAsBinding(comp, node, typedNodes, plan) {
|
|
245
|
-
try {
|
|
246
|
-
return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
return undefined;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
864
|
/**
|
|
253
865
|
* coveredFanoutNode (v3) — a FANOUT node (first-class `@refs` id-list fan-out) is covered when:
|
|
254
866
|
* - over is a static ref to a PRIOR BODY NODE's typed arr field OR an INPUT array port with elemType,
|
|
@@ -257,7 +869,7 @@ function mapAsBinding(comp, node, typedNodes, plan) {
|
|
|
257
869
|
* The fanout de-boxes to: one deduped batched dispatch → first-seen dedupe (by the elem's dedupeKey
|
|
258
870
|
* field) + dangling drop + implicitSource strip + connection wrap {items, cursor:nil} — NO boxed Value.
|
|
259
871
|
*/
|
|
260
|
-
function coveredFanoutNode(n, bodyIds, comp
|
|
872
|
+
function coveredFanoutNode(n, bodyIds, comp) {
|
|
261
873
|
if (n === null || typeof n !== "object" || !("fanout" in n))
|
|
262
874
|
return false;
|
|
263
875
|
const f = n.fanout;
|
|
@@ -277,6 +889,8 @@ function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
|
|
|
277
889
|
return false;
|
|
278
890
|
}
|
|
279
891
|
const ports = f.ports;
|
|
892
|
+
const plan = planForComp(comp);
|
|
893
|
+
const typedNodes = buildTypedNodes(comp, plan);
|
|
280
894
|
let asBinding;
|
|
281
895
|
try {
|
|
282
896
|
asBinding = { name: f.as, ref: fanoutOverElemInfo(comp, n, typedNodes, plan).elemRef, plan };
|
|
@@ -294,8 +908,8 @@ function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
|
|
|
294
908
|
* reads. A real-concurrency plan whose parallel members include a map / bindField-relation child is
|
|
295
909
|
* NOT covered here (falls through) — the static-parallel orchestration covers the componentRef fan-out
|
|
296
910
|
* (the GroupAccessRead shape); a parallel map/relation-child stage is a further shape (out of scope). */
|
|
297
|
-
function isSequentialComponentRefRead(comp
|
|
298
|
-
return coverageRejectReason(comp
|
|
911
|
+
function isSequentialComponentRefRead(comp) {
|
|
912
|
+
return coverageRejectReason(comp) === null;
|
|
299
913
|
}
|
|
300
914
|
/**
|
|
301
915
|
* coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89). Returns
|
|
@@ -305,11 +919,12 @@ function isSequentialComponentRefRead(comp, plan) {
|
|
|
305
919
|
* never disagree. This is the message the fail-closed dispatch surfaces per non-native component — the
|
|
306
920
|
* user's signal to add native coverage, fix a genuinely-dynamic spec, or use the --ir dynamic surface.
|
|
307
921
|
*/
|
|
308
|
-
function coverageRejectReason(comp
|
|
922
|
+
function coverageRejectReason(comp) {
|
|
923
|
+
const crPlan = planForComp(comp);
|
|
924
|
+
const crTypedNodes = buildTypedNodes(comp, crPlan);
|
|
309
925
|
const ep = execPlan(comp);
|
|
310
926
|
if (ep === null)
|
|
311
927
|
return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
|
|
312
|
-
const typedNodes = buildTypedNodes(comp, plan);
|
|
313
928
|
// A parallel-stage member must be a componentRef node (no map) so the static parallel orchestration
|
|
314
929
|
// is a clean fan-out — the bounded-concurrency error-precedence protocol is proven for that shape.
|
|
315
930
|
// A map child inside a parallel stage still falls through (its per-element loop is a further shape).
|
|
@@ -354,7 +969,7 @@ function coverageRejectReason(comp, plan) {
|
|
|
354
969
|
if ("fanout" in n) {
|
|
355
970
|
// v3: a first-class fanout node (deduped connection list-in fan-out) IS covered when its over is a
|
|
356
971
|
// covered ref (prior-node arr / input array with elemType) and its element ports are static.
|
|
357
|
-
if (!coveredFanoutNode(n, bodyIds, comp
|
|
972
|
+
if (!coveredFanoutNode(n, bodyIds, comp))
|
|
358
973
|
return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
|
|
359
974
|
continue;
|
|
360
975
|
}
|
|
@@ -362,7 +977,7 @@ function coverageRejectReason(comp, plan) {
|
|
|
362
977
|
// #86 pt2 batched map...into / #93 shape 2/3 / #108 guarded map (no into) / map-over-input (with
|
|
363
978
|
// elemType) ARE covered. A non-covered map shape (guard+into heterogeneous / over-input without
|
|
364
979
|
// elemType) falls through to the loud fail-closed reject.
|
|
365
|
-
if (!coveredMapNode(n, bodyIds, comp
|
|
980
|
+
if (!coveredMapNode(n, bodyIds, comp))
|
|
366
981
|
return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
|
|
367
982
|
continue;
|
|
368
983
|
}
|
|
@@ -393,7 +1008,7 @@ function coverageRejectReason(comp, plan) {
|
|
|
393
1008
|
if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
394
1009
|
return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
|
|
395
1010
|
for (const p of Object.keys(cr.ports))
|
|
396
|
-
if (portIsStatic(cr.ports[p], comp,
|
|
1011
|
+
if (portIsStatic(cr.ports[p], comp, crPlan, crTypedNodes) === false)
|
|
397
1012
|
return `node '${n.id}' port '${p}' is not statically resolvable`;
|
|
398
1013
|
}
|
|
399
1014
|
if (!outputIsNativeLowerable(comp))
|
|
@@ -401,20 +1016,25 @@ function coverageRejectReason(comp, plan) {
|
|
|
401
1016
|
return null;
|
|
402
1017
|
}
|
|
403
1018
|
/** Fail closed if a covered read SHAPE lacks the typed annotations (no silent boxed re-box). */
|
|
404
|
-
function assertTypedCoverage(comp
|
|
405
|
-
if (!isSequentialComponentRefRead(comp
|
|
1019
|
+
function assertTypedCoverage(comp) {
|
|
1020
|
+
if (!isSequentialComponentRefRead(comp))
|
|
406
1021
|
return;
|
|
407
1022
|
if (isFullyTyped(comp))
|
|
408
1023
|
return;
|
|
409
1024
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#77): component '${comp.name}' is a sequential componentRef read but lacks outType/outputType on every node — the combined emitter de-boxes the RESULT into the node's outType struct, so a covered read MUST be fully typed (bc#45). Add outType annotations, or regenerate on go-straightline-native (boxed result).`);
|
|
410
1025
|
}
|
|
411
1026
|
// ── native ports struct + PortReader ──────────────────────────────────────────────────
|
|
412
|
-
function emitPortsStruct(comp, node, plan, typedNodes
|
|
1027
|
+
function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
|
|
413
1028
|
const structName = portsStructName(comp.name, node.id);
|
|
414
1029
|
const portNames = Object.keys(node.ports);
|
|
415
1030
|
if (portNames.length === 0) {
|
|
416
1031
|
return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
|
|
417
1032
|
}
|
|
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
|
+
}
|
|
418
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));
|
|
419
1039
|
const fieldNames = portNames.map(goPortFieldName);
|
|
420
1040
|
const fields = portNames
|
|
@@ -434,7 +1054,7 @@ ${fields}
|
|
|
434
1054
|
//
|
|
435
1055
|
// The covered path no longer crosses a generic `RawValue`/`RawRow` on the RESULT plane. Each
|
|
436
1056
|
// covered node has a CONCRETE row struct `RawRow_<comp>_<node>` whose fields are the node's
|
|
437
|
-
// projected outType fields (typed, flattened)
|
|
1057
|
+
// projected outType fields (typed, flattened). A row models a SUCCESS; failure travels the (T,
|
|
438
1058
|
// per-component `Handler_<comp>` interface has one method per node returning that concrete struct;
|
|
439
1059
|
// the runner reads `row.<Field>` DIRECTLY (no `.(RawRow)`, no `.(scalar)`, no `RawValue`) and copies
|
|
440
1060
|
// each field into the node's outType struct local. The `.(T)` type-assert seam that the generic
|
|
@@ -442,7 +1062,7 @@ ${fields}
|
|
|
442
1062
|
//
|
|
443
1063
|
// A scalar/arr/opt node (outType not a named struct) has a single `Val <typed>` payload field. A
|
|
444
1064
|
// named-struct node has one Go field per outType field (same names/types as the outType decl), so
|
|
445
|
-
// `RawRow_<comp>_<node>` is structurally the outType struct
|
|
1065
|
+
// `RawRow_<comp>_<node>` is structurally the outType struct. The copier is then a
|
|
446
1066
|
// mechanical field-for-field assignment into the outType struct — NO dynamic value read anywhere.
|
|
447
1067
|
/** Row_<comp>_<node> — the concrete per-node handler-result row struct name. (Deliberately NOT
|
|
448
1068
|
* prefixed with the generic dynamic-accessor name — that accessor is what the concrete path removes.) */
|
|
@@ -474,7 +1094,7 @@ function nodeMethodName(compName, nodeId) {
|
|
|
474
1094
|
* emitRawRowStructs — emit the concrete `RawRow_<comp>_<node>` struct per covered node (and, for a
|
|
475
1095
|
* covered map node, the per-element `RawElem_<comp>_<node>`). A named-struct outType flattens to one
|
|
476
1096
|
* Go field per outType field; a scalar/arr/opt node carries a single `Val` payload. Every row carries
|
|
477
|
-
*
|
|
1097
|
+
* A row models a SUCCESS; failure is the second (error) return, so no in-band error signal.
|
|
478
1098
|
*/
|
|
479
1099
|
function emitRawRowStructs(comp, typedNodes, plan) {
|
|
480
1100
|
const parts = [];
|
|
@@ -489,7 +1109,7 @@ function emitRawRowStructs(comp, typedNodes, plan) {
|
|
|
489
1109
|
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
490
1110
|
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
491
1111
|
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
|
|
492
|
-
`type ${rawRowStructName(comp.name, n.id)} struct {\n\
|
|
1112
|
+
`type ${rawRowStructName(comp.name, n.id)} struct {\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
|
|
493
1113
|
continue;
|
|
494
1114
|
}
|
|
495
1115
|
if ("map" in n) {
|
|
@@ -502,7 +1122,7 @@ function emitRawRowStructs(comp, typedNodes, plan) {
|
|
|
502
1122
|
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
503
1123
|
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
504
1124
|
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
|
|
505
|
-
`type ${rawRowStructName(comp.name, n.id)} struct {\n\
|
|
1125
|
+
`type ${rawRowStructName(comp.name, n.id)} struct {\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
|
|
506
1126
|
continue;
|
|
507
1127
|
}
|
|
508
1128
|
parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
|
|
@@ -510,13 +1130,11 @@ function emitRawRowStructs(comp, typedNodes, plan) {
|
|
|
510
1130
|
return parts.join("\n\n");
|
|
511
1131
|
}
|
|
512
1132
|
/** Emit one concrete row struct: flatten a named outType to typed fields, else a single Val payload;
|
|
513
|
-
*
|
|
1133
|
+
* A row models a SUCCESS; a failure is the second (error) return, so no in-band error signal. */
|
|
514
1134
|
function emitOneRowStruct(name, ref, plan) {
|
|
515
1135
|
const lines = [];
|
|
516
|
-
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).`);
|
|
517
1137
|
lines.push(`type ${name} struct {`);
|
|
518
|
-
lines.push(`\tIsError bool`);
|
|
519
|
-
lines.push(`\tErr string`);
|
|
520
1138
|
if (ref.kind === "named") {
|
|
521
1139
|
const decl = findDecl(plan, ref.name);
|
|
522
1140
|
for (const f of decl.fields) {
|
|
@@ -591,21 +1209,32 @@ function emitHandlerInterface(comp, typedNodes, plan) {
|
|
|
591
1209
|
const bt = boundGoType(n, comp, typedNodes, plan);
|
|
592
1210
|
if ("fanout" in n) {
|
|
593
1211
|
// fanout (v3): one batched dispatch of the deduped id list. Ports = the batch struct; returns the
|
|
594
|
-
// 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.
|
|
595
1214
|
const portsT = `${portsStructName(comp.name, n.id)}_batch`;
|
|
596
|
-
const
|
|
597
|
-
|
|
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)`);
|
|
598
1218
|
continue;
|
|
599
1219
|
}
|
|
600
1220
|
if ("map" in n) {
|
|
601
1221
|
const m = n.map;
|
|
602
1222
|
const batched = m.batched === true;
|
|
603
1223
|
const portsT = batched ? `${portsStructName(comp.name, n.id)}_batch` : portsStructName(comp.name, n.id);
|
|
604
|
-
|
|
605
|
-
|
|
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)`);
|
|
606
1229
|
continue;
|
|
607
1230
|
}
|
|
608
|
-
|
|
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)`);
|
|
609
1238
|
}
|
|
610
1239
|
lines.push(`}`);
|
|
611
1240
|
return lines.join("\n");
|
|
@@ -629,7 +1258,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
|
|
|
629
1258
|
// struct element). A pure-static/concat port ignores the binding.
|
|
630
1259
|
const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
631
1260
|
const asBinding = { name: m.as, ref: elemRef, plan };
|
|
632
|
-
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, plan, typedNodes
|
|
1261
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan, typedNodes);
|
|
633
1262
|
const batchStruct = `${structName}_batch`;
|
|
634
1263
|
return `${elemStruct}
|
|
635
1264
|
|
|
@@ -647,7 +1276,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
|
|
|
647
1276
|
const structName = portsStructName(comp.name, node.id);
|
|
648
1277
|
const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
|
|
649
1278
|
const asBinding = { name: f.as, ref: elemRef, plan };
|
|
650
|
-
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, plan, typedNodes
|
|
1279
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan, typedNodes);
|
|
651
1280
|
const batchStruct = `${structName}_batch`;
|
|
652
1281
|
return `${elemStruct}
|
|
653
1282
|
|
|
@@ -693,18 +1322,16 @@ function fanoutOverElemInfo(comp, node, typedNodes, plan) {
|
|
|
693
1322
|
}
|
|
694
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)}).`);
|
|
695
1324
|
}
|
|
696
|
-
/** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
* 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))`). */
|
|
704
1332
|
function mapNodeArrRef(node, plan) {
|
|
705
1333
|
const outT = node.outType;
|
|
706
|
-
|
|
707
|
-
return deriveTypeRef((isArr ? outT : { arr: outT }), plan);
|
|
1334
|
+
return deriveTypeRef({ arr: outT }, plan);
|
|
708
1335
|
}
|
|
709
1336
|
/** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
|
|
710
1337
|
* type (into) or the element outType directly (no-into). This is the type of RawElem_<comp>_<node>. */
|
|
@@ -847,31 +1474,17 @@ function reconstructG(e) {
|
|
|
847
1474
|
function inStructName(compName) {
|
|
848
1475
|
return `In_${sanitize(compName)}`;
|
|
849
1476
|
}
|
|
850
|
-
/**
|
|
851
|
-
* inputPortGoType — the native Go type for an input port's declared type. Scalars lower to the concrete
|
|
852
|
-
* scalar; `array`/`map` with a declared elemType to `[]ElemT` / `map[string]ElemT`; an OPTIONAL port
|
|
853
|
-
* (`{opt:T}` → `required:false`) to `*T` — the native representation of "the value is absent" (nil).
|
|
854
|
-
* A declared type with no native lowering stays the boxed Value.
|
|
855
|
-
*
|
|
856
|
-
* An OPTIONAL port whose inner type has no native lowering FAILS CLOSED: a boxed `Value` cannot carry
|
|
857
|
-
* the presence distinction on the covered plane, and emitting the bare inner type would silently turn
|
|
858
|
-
* "absent" into a zero value. A port whose optionality cannot be represented is never silently coerced.
|
|
859
|
-
*/
|
|
1477
|
+
/** Go type for an input port's declared type (scalar → concrete; array+elemType → []ElemT; else Value). */
|
|
860
1478
|
function inputPortGoType(schema, plan, where = "input port") {
|
|
861
1479
|
const ref = inputPortTypeRef(schema, plan);
|
|
862
1480
|
if (ref !== undefined)
|
|
863
1481
|
return renderTypeRef(ref);
|
|
864
1482
|
if (inputPortIsOptional(schema))
|
|
865
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Go lowering, so "absent" has no native representation
|
|
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.`);
|
|
866
1484
|
return `${PKG}.Value`;
|
|
867
1485
|
}
|
|
868
|
-
/** The scalar kind
|
|
869
|
-
* An OPTIONAL port is deliberately NOT a scalar kind here: its native type is `*T`, and the callers of
|
|
870
|
-
* this function build REQUIRED scalar slots (`in.<Field>` read straight into a non-opt port field).
|
|
871
|
-
* Answering "int64" for an `{opt:"int"}` port is exactly how "absent" would silently become `0`. */
|
|
1486
|
+
/** The scalar kind an input port declares, or undefined if it is not a native scalar. */
|
|
872
1487
|
function inputScalarKind(schema) {
|
|
873
|
-
if (inputPortIsOptional(schema))
|
|
874
|
-
return undefined;
|
|
875
1488
|
switch (schema?.type) {
|
|
876
1489
|
case "string":
|
|
877
1490
|
case "literal": // a `"literal"` union is a constrained string (see inputPortGoType).
|
|
@@ -896,6 +1509,8 @@ function emitInStruct(comp, plan) {
|
|
|
896
1509
|
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
|
|
897
1510
|
}
|
|
898
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).
|
|
899
1514
|
const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`));
|
|
900
1515
|
const nameW = Math.max(0, ...names.map((n) => n.length));
|
|
901
1516
|
const typeW = Math.max(0, ...types.map((t) => t.length));
|
|
@@ -937,15 +1552,6 @@ function emitInDecoder(comp, plan) {
|
|
|
937
1552
|
const scalar = inputScalarKind(schema);
|
|
938
1553
|
const et = schema.elemType;
|
|
939
1554
|
lines.push(`\tif v, ok := input.Get(${JSON.stringify(p)}); ok {`);
|
|
940
|
-
if (inputPortIsOptional(schema)) {
|
|
941
|
-
// An OPTIONAL port decodes into its native *T. materializeExpr's opt de-box IS the wire rule:
|
|
942
|
-
// a nil Value (null) → nil (absent), any other value → a pointer to the materialized inner. A key
|
|
943
|
-
// that never appears leaves the struct's zero value (nil) — the same absent value. Checked FIRST:
|
|
944
|
-
// optionality wraps every inner type (an optional array is *[]T, not []T).
|
|
945
|
-
lines.push(`\t\tin.${field} = ${goTypedInternals.materializeExpr("v", inputPortTypeRef(schema, plan), plan)}`);
|
|
946
|
-
lines.push(`\t}`);
|
|
947
|
-
continue;
|
|
948
|
-
}
|
|
949
1555
|
if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
950
1556
|
// #108: decode the boxed Value array into the native []ElemT (test glue — materialize each element
|
|
951
1557
|
// via the typed materializer; a non-array or non-conforming element is a fail-closed decode error).
|
|
@@ -976,11 +1582,9 @@ function emitInDecoder(comp, plan) {
|
|
|
976
1582
|
lines.push(`\t\tin.${field} = v`);
|
|
977
1583
|
}
|
|
978
1584
|
else {
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
lines.push(`\t\
|
|
982
|
-
lines.push(`\t\t}`);
|
|
983
|
-
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)}`);
|
|
984
1588
|
}
|
|
985
1589
|
lines.push(`\t}`);
|
|
986
1590
|
}
|
|
@@ -988,17 +1592,25 @@ function emitInDecoder(comp, plan) {
|
|
|
988
1592
|
lines.push(`}`);
|
|
989
1593
|
return lines.join("\n");
|
|
990
1594
|
}
|
|
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). */
|
|
991
1599
|
/**
|
|
992
|
-
* emitPortInit — the port field
|
|
993
|
-
*
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
*
|
|
1600
|
+
* emitPortInit — emit the port field build for one port as a FULLY NATIVE Go expression (bc#90 /
|
|
1601
|
+
* runtime-free). Every covered port lowers to a concrete Go value assigned straight into the ports
|
|
1602
|
+
* struct field — NO `dslcontracts.Value`, NO nvVE helper, NO `.(T)` type-assert:
|
|
1603
|
+
* - a static string array (projection) → `[]string{"a", "b", ...}` (change #2);
|
|
1604
|
+
* - a bare int/float number literal (e.g. Query `limit: 20`) → `int64(20)` / `float64(...)`;
|
|
1605
|
+
* - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
|
|
1606
|
+
* A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered
|
|
1607
|
+
* plane — a boxed escape would re-introduce the bc-runtime coupling this module removes). Returns the
|
|
1608
|
+
* struct field initializer (`Field: <expr>`). `resolveRef` resolves a ref head for the scalar path.
|
|
997
1609
|
*/
|
|
998
1610
|
function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
|
|
999
1611
|
const c = compilePortNode(expr, comp, plan, typedNodes, isPrior, `port '${pn}'`, opts);
|
|
1000
1612
|
if (c.stmts.length > 0)
|
|
1001
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): port '${pn}' ${JSON.stringify(expr)} lowers to a FALLIBLE expression (one that can raise INT_OVERFLOW / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS). Such an expression lowers to STATEMENTS carrying an early error-return, and a port is built inline in the ports-struct literal — a position that cannot host them.
|
|
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.`);
|
|
1002
1614
|
return `${goPortFieldName(pn)}: ${c.expr}`;
|
|
1003
1615
|
}
|
|
1004
1616
|
/**
|
|
@@ -1059,9 +1671,17 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1059
1671
|
const node = comp.body[i];
|
|
1060
1672
|
const s = sanitize(node.id);
|
|
1061
1673
|
const structName = portsStructName(comp.name, comp.body[i].id);
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
|
|
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`);
|
|
1065
1685
|
lines.push(`\t\trun_${s} := false`);
|
|
1066
1686
|
lines.push(`\t\tvar ps_${s} ${structName}`);
|
|
1067
1687
|
lines.push(`\t\t_ = ps_${s}`);
|
|
@@ -1133,7 +1753,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1133
1753
|
lines.push(`\t\t\t\tdefer wg.Done()`);
|
|
1134
1754
|
lines.push(`\t\t\t\tdefer func() { <-sem }()`);
|
|
1135
1755
|
const boundArg = node.bindField !== undefined ? `bound_${s}` : "nil";
|
|
1136
|
-
|
|
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})`);
|
|
1137
1758
|
lines.push(`\t\t\t}()`);
|
|
1138
1759
|
lines.push(`\t\t}`);
|
|
1139
1760
|
}
|
|
@@ -1145,20 +1766,53 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1145
1766
|
const s = sanitize(node.id);
|
|
1146
1767
|
const ref = typedNodes.get(node.id);
|
|
1147
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.
|
|
1148
1774
|
lines.push(`\t\tif run_${s} {`);
|
|
1149
|
-
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}`);
|
|
1150
1775
|
if (meta.policy === "continue") {
|
|
1151
|
-
lines.push(`\t\t\tif
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
+
}
|
|
1154
1793
|
lines.push(`\t\t\t}`);
|
|
1155
1794
|
}
|
|
1156
1795
|
else {
|
|
1157
|
-
|
|
1158
|
-
lines.push(`\t\t\
|
|
1159
|
-
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)`);
|
|
1160
1798
|
lines.push(`\t\t\t}`);
|
|
1161
|
-
|
|
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
|
+
}
|
|
1162
1816
|
lines.push(`\t\t\tproduced_${s} = true`);
|
|
1163
1817
|
}
|
|
1164
1818
|
lines.push(`\t\t}`);
|
|
@@ -1318,22 +1972,75 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1318
1972
|
}
|
|
1319
1973
|
lines.push(`${indent}ports_${s} := ${structName}{${inits.join(", ")}}`);
|
|
1320
1974
|
const oc = `row_${s}`;
|
|
1321
|
-
lines.push(`${indent}${oc}, ${oc}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg})`);
|
|
1322
|
-
lines.push(`${indent}if !${oc}Resolved {`, `${indent}\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${node.component}' has no handler (fail-closed)"`)}`, `${indent}}`);
|
|
1323
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;
|
|
1324
1984
|
if (meta.policy === "continue") {
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
+
}
|
|
1329
2009
|
}
|
|
1330
2010
|
else {
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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
|
+
}
|
|
1337
2044
|
}
|
|
1338
2045
|
for (const c of closers)
|
|
1339
2046
|
lines.push(c);
|
|
@@ -1408,7 +2115,8 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1408
2115
|
if (!hasGuard)
|
|
1409
2116
|
return null;
|
|
1410
2117
|
const resolveHead = goNativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding: { name: asName, ref: overElemRef, plan }, asBase: elemVar });
|
|
1411
|
-
|
|
2118
|
+
const be = makeGoExprBackend(zeroOut, resolveHead, plan);
|
|
2119
|
+
return new NativeExprCompiler(be).compileBool(m.when);
|
|
1412
2120
|
};
|
|
1413
2121
|
// emit the per-element native ports struct build (shared; `il` is the indent inside the element
|
|
1414
2122
|
// loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
|
|
@@ -1431,6 +2139,12 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1431
2139
|
lines.push(`${indent}over_${s} := ${overExpr}`);
|
|
1432
2140
|
lines.push(`${indent}${typedLocal(node.id)} = make(${elemGo}, 0, len(over_${s}))`);
|
|
1433
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) : "";
|
|
1434
2148
|
// emit the per-element guard keep-gate (#108). `il` = indent, `elemVar` = the over element loop var.
|
|
1435
2149
|
// On skip: batched → `continue` (element excluded from items + keptOver); non-batched → `continue`.
|
|
1436
2150
|
const emitGuard = (il, elemVar) => {
|
|
@@ -1447,7 +2161,7 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1447
2161
|
};
|
|
1448
2162
|
if (batched) {
|
|
1449
2163
|
// BATCHED (#86 pt2 into / #93 shape 3 no-into / #108 guarded no-into): build kept-element ports,
|
|
1450
|
-
// dispatch ONCE. The concrete Node_* returns RawRow{
|
|
2164
|
+
// dispatch ONCE. The concrete Node_* returns RawRow{Rows []RawElem} aligned to items (or an error).
|
|
1451
2165
|
// With a guard, `items` (and, for into, `keptOver`) hold only kept elements — matching run_behavior's
|
|
1452
2166
|
// keptIdx-aligned batch. `into` is never combined with a guard (rejected). no-into collects each row.
|
|
1453
2167
|
lines.push(`${indent}items_${s} := make([]${structName}, 0, len(over_${s}))`);
|
|
@@ -1465,22 +2179,39 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1465
2179
|
// empty items => empty result (handler not called), matching run_behavior.
|
|
1466
2180
|
lines.push(`${indent}if len(items_${s}) > 0 {`);
|
|
1467
2181
|
lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: items_${s}}`);
|
|
1468
|
-
lines.push(`${indent}\tmo_${s}, mo_${s}
|
|
1469
|
-
lines.push(`${indent}\tif
|
|
1470
|
-
|
|
1471
|
-
|
|
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}`);
|
|
1472
2189
|
if (hasInto) {
|
|
1473
2190
|
// #86 pt2: zip-attach the into field onto each kept over element (augmented element copier).
|
|
1474
2191
|
lines.push(`${indent}\tfor mk_${s} := range keptOver_${s} {`);
|
|
1475
|
-
|
|
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
|
+
}
|
|
1476
2200
|
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1477
2201
|
lines.push(`${indent}\t}`);
|
|
1478
2202
|
}
|
|
1479
2203
|
else {
|
|
1480
2204
|
// #93 shape 3 / #108: collect each element DIRECTLY from the aligned per-element row (field copy).
|
|
1481
|
-
lines.push(`${indent}\tfor mk_${s} :=
|
|
2205
|
+
lines.push(`${indent}\tfor mk_${s} := 0; mk_${s} < ${batchLen}; mk_${s}++ {`);
|
|
1482
2206
|
lines.push(`${indent}\t\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
|
|
1483
|
-
|
|
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
|
+
}
|
|
1484
2215
|
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1485
2216
|
lines.push(`${indent}\t}`);
|
|
1486
2217
|
}
|
|
@@ -1499,15 +2230,30 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1499
2230
|
// per-element dispatch (ONE physical request per element). run_behavior passes the over element
|
|
1500
2231
|
// as the bound value; the element data is fully conveyed by the native ports struct here, so bound
|
|
1501
2232
|
// stays nil. Byte-equivalence holds because the bound value only feeds handler-internal logic.
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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
|
+
}
|
|
1505
2251
|
if (hasInto) {
|
|
1506
|
-
lines.push(`${indent}\tel_${s} := ${elemMat}(${elemLocal(s)},
|
|
2252
|
+
lines.push(`${indent}\tel_${s} := ${elemMat}(${elemLocal(s)}, ${elemVar})`);
|
|
1507
2253
|
}
|
|
1508
2254
|
else {
|
|
1509
2255
|
lines.push(`${indent}\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
|
|
1510
|
-
lines.push(emitRowCopy(`el_${s}`,
|
|
2256
|
+
lines.push(emitRowCopy(`el_${s}`, elemVar, elemRowRef, plan, indent.length + 1));
|
|
1511
2257
|
}
|
|
1512
2258
|
lines.push(`${indent}\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1513
2259
|
lines.push(`${indent}}`);
|
|
@@ -1578,19 +2324,25 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
|
|
|
1578
2324
|
// empty id-list => empty connection (handler not called), matching run_behavior.
|
|
1579
2325
|
lines.push(`${indent}if len(pi_${s}) > 0 {`);
|
|
1580
2326
|
lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: pi_${s}}`);
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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}`);
|
|
1585
2334
|
// ── THE ONE dedupe/drop (mirrors fanoutDedupDrop verbatim): first-seen dedupe by dedupeKey field;
|
|
1586
2335
|
// drop dangling (zero dedupeKey ≡ interpreter's null/absent-key body); implicitSource strip is
|
|
1587
2336
|
// structural (the elem type already omits it). cursor is always nil (connection wrap).
|
|
1588
|
-
lines.push(`${indent}\tseen_${s} := make(map[string]struct{}, len(fo_${s}
|
|
1589
|
-
lines.push(`${indent}\titems_${s} = make([]${elemGo}, 0, len(fo_${s}
|
|
1590
|
-
lines.push(`${indent}\tfor fi_${s} := range fo_${s}
|
|
1591
|
-
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} {`);
|
|
1592
2340
|
lines.push(`${indent}\t\tvar el_${s} ${elemGo}`);
|
|
1593
|
-
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}`);
|
|
1594
2346
|
lines.push(`${indent}\t\tkey_${s} := el_${s}.${dkGo}`);
|
|
1595
2347
|
if (f.drop === "dangling") {
|
|
1596
2348
|
// dangling: a body whose dedupeKey is the zero value (empty string) is dropped.
|
|
@@ -1609,15 +2361,12 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
|
|
|
1609
2361
|
lines.push(c);
|
|
1610
2362
|
return lines.join("\n");
|
|
1611
2363
|
}
|
|
1612
|
-
/**
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1615
|
-
* so an OPTIONAL port resolves as `{opt:…}` (native `*T`) and the compiler can see its optionality: a
|
|
1616
|
-
* null-compare becomes a real presence test and a coalesce a real deref-or-default, while a
|
|
1617
|
-
* REQUIRED-scalar slot fed by an opt fails closed. Go values are read directly off the input struct (no
|
|
1618
|
-
* ownership ceremony); a type with no native lowering → null (fail-closed).
|
|
1619
|
-
*/
|
|
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). */
|
|
1620
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.
|
|
1621
2370
|
const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
|
|
1622
2371
|
if (ref === undefined)
|
|
1623
2372
|
return null;
|
|
@@ -1635,7 +2384,11 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut)
|
|
|
1635
2384
|
const c = node.cond;
|
|
1636
2385
|
const s = sanitize(node.id);
|
|
1637
2386
|
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1638
|
-
const resolveHead =
|
|
2387
|
+
const resolveHead = (head) => {
|
|
2388
|
+
if (priorHere(head))
|
|
2389
|
+
return { expr: typedLocal(head), ref: typedNodes.get(head) };
|
|
2390
|
+
return goInputHeadRef(head, comp, plan);
|
|
2391
|
+
};
|
|
1639
2392
|
const be = makeGoExprBackend(zeroOut, resolveHead, plan);
|
|
1640
2393
|
const compiler = new NativeExprCompiler(be);
|
|
1641
2394
|
const cond = compiler.compileBool(c.if);
|
|
@@ -1866,14 +2619,11 @@ function makeGoExprBackend(zeroOut, resolveHead, plan) {
|
|
|
1866
2619
|
// opt (`*T`) null-presence test: present = `(expr != nil)`, absent = `(expr == nil)`.
|
|
1867
2620
|
optIsSome: (expr) => `(${expr} != nil)`,
|
|
1868
2621
|
optIsNone: (expr) => `(${expr} == nil)`,
|
|
1869
|
-
// opt (`*T`) defaulted to a PURE default
|
|
1870
|
-
//
|
|
1871
|
-
// pointer ONCE, deref it when present, else yield the default. The default sits in the else branch,
|
|
1872
|
-
// so it is evaluated only when absent — run_behavior's lazy right side exactly.
|
|
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).
|
|
1873
2624
|
optUnwrapOr: (optExpr, defaultExpr, innerTy) => `func() ${innerTy} { if v := ${optExpr}; v != nil { return *v }; return ${defaultExpr} }()`,
|
|
1874
|
-
// opt defaulted to a FALLIBLE default — a statement if/else
|
|
1875
|
-
//
|
|
1876
|
-
// 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.
|
|
1877
2627
|
optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
|
|
1878
2628
|
`var ${temp} ${ty}`,
|
|
1879
2629
|
`if ${bindTemp} := ${optExpr}; ${bindTemp} != nil {`,
|
|
@@ -2037,13 +2787,15 @@ var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name
|
|
|
2037
2787
|
* mismatch Sprintf, `strings` for a concat builder, `sync` for a parallel stage). ExpectedSpecVersions
|
|
2038
2788
|
* stays as a local `var` for provenance, but the old `dslcontracts.SpecVersions` skew `init()` is GONE
|
|
2039
2789
|
* (change #5 — there is no runtime to skew against once the module is runtime-free). */
|
|
2040
|
-
function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath) {
|
|
2790
|
+
function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath, needStrconv = false) {
|
|
2041
2791
|
const sv = ctx.specVersions;
|
|
2042
2792
|
const imports = [];
|
|
2043
2793
|
if (needFmt)
|
|
2044
2794
|
imports.push(`\t"fmt"`);
|
|
2045
2795
|
if (needMath)
|
|
2046
2796
|
imports.push(`\t"math"`);
|
|
2797
|
+
if (needStrconv)
|
|
2798
|
+
imports.push(`\t"strconv"`);
|
|
2047
2799
|
if (needStrings)
|
|
2048
2800
|
imports.push(`\t"strings"`);
|
|
2049
2801
|
if (needSync)
|
|
@@ -2063,14 +2815,12 @@ var ExpectedSpecVersions = map[string]int64{"behavior": ${sv.behavior}, "express
|
|
|
2063
2815
|
}
|
|
2064
2816
|
// ── module assembly ──────────────────────────────────────────────────────────────────
|
|
2065
2817
|
function emit(ctx) {
|
|
2066
|
-
// The type plan is the SSoT the coverage gate + the port lowering both read (the gate ASKS the port
|
|
2067
|
-
// lowering whether a port lowers), so it is built up-front — before the coverage checks.
|
|
2068
|
-
const plan = buildTypePlan(ctx.ir);
|
|
2069
2818
|
// Fail closed if a covered read SHAPE lacks the typed annotations (no silent re-box).
|
|
2070
2819
|
for (const c of ctx.ir.components)
|
|
2071
|
-
assertTypedCoverage(c
|
|
2072
|
-
const native = ctx.ir.components.filter(
|
|
2073
|
-
const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c
|
|
2820
|
+
assertTypedCoverage(c);
|
|
2821
|
+
const native = ctx.ir.components.filter(isNativeRaw);
|
|
2822
|
+
const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
|
|
2823
|
+
const plan = buildTypePlan(ctx.ir);
|
|
2074
2824
|
const decls = emitTypeDecls(plan);
|
|
2075
2825
|
// #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler when it emits
|
|
2076
2826
|
// a fallible checked-arith helper call — determines whether the helper library + `math` import ship).
|
|
@@ -2083,9 +2833,16 @@ function emit(ctx) {
|
|
|
2083
2833
|
const handlerIfaces = [];
|
|
2084
2834
|
const elemCopiers = [];
|
|
2085
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();
|
|
2086
2839
|
for (const c of native) {
|
|
2087
2840
|
inStructs.push(emitInStruct(c, plan));
|
|
2088
|
-
const typedNodes =
|
|
2841
|
+
const typedNodes = new Map();
|
|
2842
|
+
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
2843
|
+
for (const n of c.body)
|
|
2844
|
+
if (!isControlGate(n))
|
|
2845
|
+
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan)); // #125: control-gate typeless; map は produced-array ref に正規化
|
|
2089
2846
|
for (const n of c.body) {
|
|
2090
2847
|
if ("fanout" in n) {
|
|
2091
2848
|
// v3: a fanout node emits a per-id element ports struct + a batch struct (like a batched map).
|
|
@@ -2101,12 +2858,16 @@ function emit(ctx) {
|
|
|
2101
2858
|
}
|
|
2102
2859
|
else {
|
|
2103
2860
|
// #129: pass typedNodes so a leaf array port fed by a prior node's arr outType types as []ElemT.
|
|
2104
|
-
structs.push(emitPortsStruct(c, n, plan, typedNodes));
|
|
2861
|
+
structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes));
|
|
2105
2862
|
}
|
|
2106
2863
|
}
|
|
2864
|
+
// fail closed for a record inside a non-named top (never a silent trust) — before emitting anything.
|
|
2865
|
+
assertDeBoxableResults(c, typedNodes, plan);
|
|
2107
2866
|
// CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
|
|
2108
2867
|
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
2109
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);
|
|
2110
2871
|
// augmented-element copiers for covered map...into nodes (over fields + concrete into row).
|
|
2111
2872
|
const mm = emitMapElemMaterializers(c, typedNodes, plan);
|
|
2112
2873
|
if (mm)
|
|
@@ -2130,7 +2891,7 @@ function emit(ctx) {
|
|
|
2130
2891
|
// minimal, runtime-free header.
|
|
2131
2892
|
if (nonNative.length > 0) {
|
|
2132
2893
|
const rejects = nonNative
|
|
2133
|
-
.map((c) => ` - component '${c.name}': ${coverageRejectReason(c
|
|
2894
|
+
.map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
|
|
2134
2895
|
.join("\n");
|
|
2135
2896
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (native codegen) surface: ${nonNative.length} component(s) are NOT a covered native read:\n${rejects}\n\n` +
|
|
2136
2897
|
`The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
|
|
@@ -2140,7 +2901,9 @@ function emit(ctx) {
|
|
|
2140
2901
|
// #108: `math` is imported iff the native-expr compiler emitted a fallible helper (bcExpr_modF uses
|
|
2141
2902
|
// math.Mod). The helper library itself is baked into the module only when used (goExprUsed.helpers).
|
|
2142
2903
|
const needMath = goExprUsed.helpers;
|
|
2143
|
-
|
|
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);
|
|
2144
2907
|
const banner = `// COMBINED READ layer (bc#90 — the RUNTIME-FREE read de-box; STRUCT-ONLY, FULLY-NATIVE surface). A
|
|
2145
2908
|
// covered read is a strictly-sequential typed componentRef chain (point reads + relationKind:single
|
|
2146
2909
|
// children), emitted ONLY here: each node builds a native ports struct by direct field assignment (every
|
|
@@ -2164,6 +2927,12 @@ function emit(ctx) {
|
|
|
2164
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")}`
|
|
2165
2928
|
: undefined,
|
|
2166
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,
|
|
2167
2936
|
elemCopiers.length
|
|
2168
2937
|
? `// Augmented map-element copiers (over element fields + the concrete into row — pure assignment).\n${elemCopiers.join("\n\n")}`
|
|
2169
2938
|
: undefined,
|
|
@@ -2188,8 +2957,8 @@ function emit(ctx) {
|
|
|
2188
2957
|
*/
|
|
2189
2958
|
export function goTypedNativeObserve(ir, runtimeImport) {
|
|
2190
2959
|
const components = ir.components;
|
|
2960
|
+
const native = components.filter(isNativeRaw);
|
|
2191
2961
|
const plan = buildTypePlan(ir);
|
|
2192
|
-
const native = components.filter((c) => isNativeRaw(c, plan));
|
|
2193
2962
|
const serializers = emitSerializers(plan);
|
|
2194
2963
|
const marshallers = goTypedInternals.emitMarshallers(plan);
|
|
2195
2964
|
const mustHelpers = emitMustHelpers(plan);
|
|
@@ -2209,6 +2978,199 @@ export function goTypedNativeObserve(ir, runtimeImport) {
|
|
|
2209
2978
|
// input decoders (TEST glue): generic *Obj -> concrete In_<comp>. Scalar fields read the typed value
|
|
2210
2979
|
// (a `.(T)` here is on the SDK/wire Value in the OBSERVE companion, never in the covered module).
|
|
2211
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
|
+
: "";
|
|
2212
3174
|
// The observe companion drives EVERY covered component, so its handler type param H must satisfy
|
|
2213
3175
|
// EVERY per-component concrete Handler_<comp> interface — a combined constraint embeds them all.
|
|
2214
3176
|
const combinedConstraint = `allNativeRawHandlers`;
|
|
@@ -2234,35 +3196,56 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
|
2234
3196
|
continue; // #108: cond has no handler method (pure Expression) — no scripted impl.
|
|
2235
3197
|
const ref = typedNodes.get(n.id);
|
|
2236
3198
|
if ("fanout" in n) {
|
|
2237
|
-
// v3: scripted fanout impl — drain one batched outcome and decode the aligned []Value into the
|
|
2238
|
-
// per-body elem rows (a null array element materializes to the zero elem row = dangling).
|
|
2239
3199
|
const f = n.fanout;
|
|
2240
|
-
const
|
|
3200
|
+
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
2241
3201
|
const elemT = rawElemStructName(c.name, n.id);
|
|
2242
3202
|
const batchT = rawRowStructName(c.name, n.id);
|
|
2243
|
-
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2244
3203
|
const portsT = `${portsStructName(c.name, n.id)}_batch`;
|
|
2245
3204
|
const foBoundT = boundGoType(n, c, typedNodes, plan);
|
|
2246
|
-
|
|
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) {
|
|
2247
3232
|
src, ok := s.next(${JSON.stringify(f.component)})
|
|
2248
3233
|
if !ok {
|
|
2249
|
-
return ${batchT}{},
|
|
3234
|
+
return ${batchT}{}, unknownComponent(${JSON.stringify(f.component)})
|
|
2250
3235
|
}
|
|
2251
3236
|
if src.isErr {
|
|
2252
|
-
return ${batchT}{
|
|
3237
|
+
return ${batchT}{}, leafFailure(src.err)
|
|
2253
3238
|
}
|
|
2254
3239
|
arr, _ := src.ok.([]${PKG}.Value)
|
|
2255
3240
|
rows := make([]${elemT}, 0, len(arr))
|
|
2256
3241
|
for _, ev := range arr {
|
|
2257
|
-
// v3: a null aligned body is a DANGLING id — decode it to the zero elem row (empty dedupeKey
|
|
2258
|
-
// field), so the runner's dangling-drop excludes it (byte-equal to fanoutDedupDrop).
|
|
2259
3242
|
if ev == nil {
|
|
2260
3243
|
rows = append(rows, ${elemT}{})
|
|
2261
3244
|
continue
|
|
2262
3245
|
}
|
|
2263
3246
|
rows = append(rows, ${elemDecode}(ev))
|
|
2264
3247
|
}
|
|
2265
|
-
return ${batchT}{Rows: rows},
|
|
3248
|
+
return ${batchT}{Rows: rows}, nil
|
|
2266
3249
|
}`);
|
|
2267
3250
|
continue;
|
|
2268
3251
|
}
|
|
@@ -2270,77 +3253,172 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
|
2270
3253
|
const m = n.map;
|
|
2271
3254
|
const batched = m.batched === true;
|
|
2272
3255
|
const arrRef = ref;
|
|
2273
|
-
const
|
|
3256
|
+
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
2274
3257
|
const elemT = rawElemStructName(c.name, n.id);
|
|
2275
3258
|
const batchT = rawRowStructName(c.name, n.id);
|
|
2276
|
-
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2277
3259
|
const portsT = batched ? `${portsStructName(c.name, n.id)}_batch` : portsStructName(c.name, n.id);
|
|
2278
|
-
const retT = batched ? batchT : elemT;
|
|
2279
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");
|
|
2280
3296
|
if (batched) {
|
|
2281
|
-
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) {
|
|
2282
3298
|
src, ok := s.next(${JSON.stringify(m.component)})
|
|
2283
3299
|
if !ok {
|
|
2284
|
-
return ${batchT}{},
|
|
3300
|
+
return ${batchT}{}, unknownComponent(${JSON.stringify(m.component)})
|
|
2285
3301
|
}
|
|
2286
3302
|
if src.isErr {
|
|
2287
|
-
return ${batchT}{
|
|
3303
|
+
return ${batchT}{}, leafFailure(src.err)
|
|
2288
3304
|
}
|
|
2289
3305
|
arr, _ := src.ok.([]${PKG}.Value)
|
|
2290
3306
|
rows := make([]${elemT}, 0, len(arr))
|
|
2291
3307
|
for _, ev := range arr {
|
|
2292
3308
|
rows = append(rows, ${elemDecode}(ev))
|
|
2293
3309
|
}
|
|
2294
|
-
return ${batchT}{Rows: rows},
|
|
3310
|
+
return ${batchT}{Rows: rows}, nil
|
|
2295
3311
|
}`);
|
|
2296
3312
|
}
|
|
2297
3313
|
else {
|
|
2298
|
-
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) {
|
|
2299
3315
|
src, ok := s.next(${JSON.stringify(m.component)})
|
|
2300
3316
|
if !ok {
|
|
2301
|
-
return ${elemT}{},
|
|
3317
|
+
return ${elemT}{}, unknownComponent(${JSON.stringify(m.component)})
|
|
2302
3318
|
}
|
|
2303
3319
|
if src.isErr {
|
|
2304
|
-
return ${elemT}{
|
|
3320
|
+
return ${elemT}{}, leafFailure(src.err)
|
|
2305
3321
|
}
|
|
2306
|
-
return ${elemDecode}(src.ok),
|
|
3322
|
+
return ${elemDecode}(src.ok), nil
|
|
2307
3323
|
}`);
|
|
2308
3324
|
}
|
|
2309
|
-
void retT;
|
|
2310
3325
|
continue;
|
|
2311
3326
|
}
|
|
2312
3327
|
const node = n;
|
|
2313
3328
|
const rowT = rawRowStructName(c.name, node.id);
|
|
2314
|
-
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
2315
3329
|
const nodeBoundT = boundGoType(node, c, typedNodes, plan);
|
|
2316
|
-
//
|
|
2317
|
-
//
|
|
2318
|
-
//
|
|
2319
|
-
//
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
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) {
|
|
2336
3390
|
src, ok := s.next(${JSON.stringify(node.component)})
|
|
2337
3391
|
if !ok {
|
|
2338
|
-
return ${
|
|
3392
|
+
return nil, unknownComponent(${JSON.stringify(node.component)})
|
|
2339
3393
|
}
|
|
2340
3394
|
if src.isErr {
|
|
2341
|
-
return
|
|
3395
|
+
return nil, leafFailure(src.err)
|
|
2342
3396
|
}
|
|
2343
|
-
${
|
|
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
|
|
2344
3422
|
}`);
|
|
2345
3423
|
}
|
|
2346
3424
|
}
|
|
@@ -2383,15 +3461,13 @@ func NewScriptedNativeRaw(spec *${PKG}.JObj) (*ScriptedNativeRaw, error) {
|
|
|
2383
3461
|
for _, e := range entries {
|
|
2384
3462
|
eo, _ := e.(*${PKG}.JObj)
|
|
2385
3463
|
// #129: an \`echo\` directive names an input port to echo back (mirrors the reference
|
|
2386
|
-
// scriptedHandlers'
|
|
2387
|
-
// payload. \`{"echo":"port","port":"X"}\` is normalized to the port NAME here, so the
|
|
2388
|
-
// generated node method just compares \`src.echo\` against its echoable port names —
|
|
2389
|
-
// \`{"echo":"items"}\` is already that shape.
|
|
3464
|
+
// scriptedHandlers' \`echo === "items"\`). It is drained together with the ok payload.
|
|
2390
3465
|
echo := ""
|
|
2391
3466
|
if echoNode, has := eo.Get("echo"); has {
|
|
2392
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.
|
|
2393
3470
|
if echo == "port" {
|
|
2394
|
-
echo = ""
|
|
2395
3471
|
if portNode, hasPort := eo.Get("port"); hasPort {
|
|
2396
3472
|
echo, _ = portNode.(string)
|
|
2397
3473
|
}
|
|
@@ -2441,11 +3517,14 @@ func (s *ScriptedNativeRaw) next(component string) (scriptSrc, bool) {
|
|
|
2441
3517
|
package behaviors
|
|
2442
3518
|
|
|
2443
3519
|
import (
|
|
2444
|
-
"sync"
|
|
3520
|
+
${anyDeBox ? '\t"strconv"\n\n' : ""} "sync"
|
|
2445
3521
|
|
|
2446
3522
|
${PKG} "${runtimeImport ?? "github.com/foo-ogawa/behavior-contracts/go"}"
|
|
2447
3523
|
)
|
|
2448
3524
|
|
|
3525
|
+
// scripted wire adapter (TEST glue) — wraps a scripted Value as a WireRow the generated de-box probes.
|
|
3526
|
+
${wireAdapter}
|
|
3527
|
+
|
|
2449
3528
|
// typed -> Value serializers (TEST glue only — the covered module has none).
|
|
2450
3529
|
${serializers}
|
|
2451
3530
|
|
|
@@ -2479,14 +3558,6 @@ ${arms}
|
|
|
2479
3558
|
}
|
|
2480
3559
|
`;
|
|
2481
3560
|
}
|
|
2482
|
-
/** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
|
|
2483
|
-
* declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row (a scalar/arr/
|
|
2484
|
-
* opt row via its Val; a NAMED row via its flattened fields copied from the struct port). A port with a
|
|
2485
|
-
* different type is not echoable (the row could not hold it). */
|
|
2486
|
-
function echoablePorts(comp, node, ref, plan, typedNodes) {
|
|
2487
|
-
const want = renderTypeRef(ref);
|
|
2488
|
-
return Object.keys(node.ports).filter((p) => portFieldGoType(node.ports[p], comp, plan, typedNodes, `port '${p}'`) === want);
|
|
2489
|
-
}
|
|
2490
3561
|
/** emitDecodeRow — emit (once, deduped) a decoder `decodeRow_<comp>_<node><suffix>(v Value) <rowT>`
|
|
2491
3562
|
* that materializes the scripted ok Value into the concrete row struct (typed fields = the outType /
|
|
2492
3563
|
* elem-row type). Named: reuse the Value->struct marshaller + copy fields; scalar/arr/opt: materialize
|