behavior-contracts 0.8.6 → 0.8.8
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/cli-contract.yaml +189 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +76 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/codegen.d.ts +6 -0
- package/dist/commands/codegen.d.ts.map +1 -0
- package/dist/commands/codegen.js +39 -0
- package/dist/commands/codegen.js.map +1 -0
- package/dist/generated/commands.d.ts +8 -0
- package/dist/generated/commands.d.ts.map +1 -0
- package/dist/generated/commands.js +55 -0
- package/dist/generated/commands.js.map +1 -0
- package/dist/generated/contract.d.ts +3 -0
- package/dist/generated/contract.d.ts.map +1 -0
- package/dist/generated/contract.js +5 -0
- package/dist/generated/contract.js.map +1 -0
- package/dist/generated/index.d.ts +7 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/index.js +7 -0
- package/dist/generated/index.js.map +1 -0
- package/dist/generated/program.d.ts +17 -0
- package/dist/generated/program.d.ts.map +1 -0
- package/dist/generated/program.js +117 -0
- package/dist/generated/program.js.map +1 -0
- package/dist/generated/schemas.d.ts +2 -0
- package/dist/generated/schemas.d.ts.map +1 -0
- package/dist/generated/schemas.js +3 -0
- package/dist/generated/schemas.js.map +1 -0
- package/dist/generated/types.d.ts +35 -0
- package/dist/generated/types.d.ts.map +1 -0
- package/dist/generated/types.js +3 -0
- package/dist/generated/types.js.map +1 -0
- package/dist/generator/core.d.ts +28 -0
- package/dist/generator/core.d.ts.map +1 -1
- package/dist/generator/core.js +7 -0
- package/dist/generator/core.js.map +1 -1
- package/dist/generator/emit-shared-debox.d.ts +108 -0
- package/dist/generator/emit-shared-debox.d.ts.map +1 -0
- package/dist/generator/emit-shared-debox.js +117 -0
- package/dist/generator/emit-shared-debox.js.map +1 -0
- package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-go.js +436 -1078
- package/dist/generator/emit-typed-native-go.js.map +1 -1
- package/dist/generator/emit-typed-native-rust.d.ts +9 -7
- package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-rust.js +313 -844
- package/dist/generator/emit-typed-native-rust.js.map +1 -1
- package/dist/generator/typed.d.ts +4 -2
- package/dist/generator/typed.d.ts.map +1 -1
- package/dist/generator/typed.js +4 -2
- package/dist/generator/typed.js.map +1 -1
- package/package.json +12 -2
|
@@ -5,7 +5,20 @@ import { goTypedInternals } from "./emit-shared-go-typed.js";
|
|
|
5
5
|
import { buildTypePlan, deriveTypeRef, findDecl, inputPortIsOptional, inputPortTypeRef } from "./typed.js";
|
|
6
6
|
import { concurrencyPlan, execPlan, analyzeSequentialOps, classifyExpr, buildComponentPlan, } from "./straightline.js";
|
|
7
7
|
import { NativeExprCompiler } from "./native-expr.js";
|
|
8
|
+
import { buildDeBoxPlan, notationOfRef } from "./emit-shared-debox.js";
|
|
8
9
|
const PKG = "dslcontracts";
|
|
10
|
+
/** resolveGoWire — the CONCRETE consumer wire type names the de-box classifies a node result against
|
|
11
|
+
* (bc#141). Default (no option) → the in-package convention names, unqualified. A configured `import`
|
|
12
|
+
* qualifies the names with the imported package's selector so a consumer can keep its wire types in its
|
|
13
|
+
* own package. `value`/`row`/`list` name the WireValue / WireRow / WireList concrete types; the covered
|
|
14
|
+
* runner + the generated Probe result structs reference them by these names. */
|
|
15
|
+
function resolveGoWire(ctx) {
|
|
16
|
+
const w = ctx.goWire;
|
|
17
|
+
if (w === undefined)
|
|
18
|
+
return { value: "WireValue", row: "WireRow", list: "WireList" };
|
|
19
|
+
const sel = w.import !== undefined ? `${w.import.split("/").pop().replace(/[^A-Za-z0-9_]/g, "_")}.` : "";
|
|
20
|
+
return { value: `${sel}${w.value}`, row: `${sel}${w.row}`, list: `${sel}${w.list}`, import: w.import };
|
|
21
|
+
}
|
|
9
22
|
/** goErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
|
|
10
23
|
* plane (runtime-free). The covered module declares `type BehaviorError struct { Code, Message string }`
|
|
11
24
|
* (see emitBehaviorErrorType); a `*BehaviorError` satisfies the runner's `error` return. This REPLACES
|
|
@@ -100,60 +113,184 @@ const { goFieldName, renderTypeRef, goScalar, serializeTyped, emitTypeDecls, emi
|
|
|
100
113
|
function typedLocal(nodeId) {
|
|
101
114
|
return `t_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
102
115
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
case "
|
|
127
|
-
|
|
116
|
+
// ── strict de-box: the ONE inline renderer over the SSoT plan ─────────────────────────────
|
|
117
|
+
//
|
|
118
|
+
// A covered node result de-box is compiled INLINE at its consumption point from the language-neutral
|
|
119
|
+
// SSoT walk (emit-shared-debox.ts `buildDeBoxPlan`). The handler returns the consumer's WIRE (a single
|
|
120
|
+
// concrete WireValue — the producer's datum); the generated de-box classifies it against the STATICALLY
|
|
121
|
+
// declared node type via the CONCRETE seam methods (WireValue.As* → the top; WireRow.Probe* → a record
|
|
122
|
+
// field / map value; WireList.Elem* → a list element). The consumer supplies variant classification (a
|
|
123
|
+
// probe reports the producer's own wire tag as a free string); the generated code owns strictness —
|
|
124
|
+
// required/optional, present/absent, error assembly, fail-closed. NO decodeWire_* helper, NO output-side
|
|
125
|
+
// recursion, NO interface, NO generics, NO boxed Value: a nested record expands to a nested struct
|
|
126
|
+
// build, a collection to a nested loop, and every method call resolves to a concrete consumer type.
|
|
127
|
+
/** goTypeOfPlan — the CONCRETE Go type of a de-box plan node (base kind + `optDepth` `*` wraps). Equal to
|
|
128
|
+
* renderTypeRef of the ORIGINAL ref, but read off the language-neutral plan so the inline renderer can
|
|
129
|
+
* declare its temps / list-element / map-value locals without re-deriving the TypeRef. */
|
|
130
|
+
function goTypeOfPlan(plan) {
|
|
131
|
+
let base;
|
|
132
|
+
switch (plan.k) {
|
|
133
|
+
case "scalar":
|
|
134
|
+
base = goScalar(plan.scalar);
|
|
135
|
+
break;
|
|
136
|
+
case "record":
|
|
137
|
+
base = plan.typeName;
|
|
138
|
+
break;
|
|
139
|
+
case "list":
|
|
140
|
+
base = `[]${goTypeOfPlan(plan.elem)}`;
|
|
141
|
+
break;
|
|
128
142
|
case "map":
|
|
129
|
-
|
|
143
|
+
base = `map[string]${goTypeOfPlan(plan.value)}`;
|
|
144
|
+
break;
|
|
130
145
|
}
|
|
146
|
+
return "*".repeat(plan.optDepth) + base;
|
|
131
147
|
}
|
|
132
|
-
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
/**
|
|
149
|
+
* renderGoDeBox — the ONE inline, recursive strict de-box renderer (Go). Folds a language-neutral
|
|
150
|
+
* {@link DeBoxPlan} (the SSoT walk) into FLAT Go STATEMENTS that decode the wire obtained by probing
|
|
151
|
+
* `srcExpr` per the node's access (top → `As*()`, field → `Probe*(key)`, elem → `Elem*(i)`, mapval →
|
|
152
|
+
* `Probe*(k)`) and assign the decoded native value into the lvalue `target`. The recursion runs HERE at
|
|
153
|
+
* generation time (walking the plan), so the emitted Go is fully unrolled: NO `decodeWire_*` helper, NO
|
|
154
|
+
* output-side recursion — a nested record expands to a nested struct build, a nested collection to a
|
|
155
|
+
* nested loop. A required-position mismatch runs `fail(...)` (default: `return <zero>, de_*(...)`); an
|
|
156
|
+
* optional position (optDepth>0) leaves the target pointer nil on absent/null. `ctr` hands out unique
|
|
157
|
+
* temp suffixes so nested blocks never collide; `idxExpr` is the element index / map-key argument the
|
|
158
|
+
* enclosing list / map loop supplies (empty for top / field). Mirrors the interpreter's outType check,
|
|
159
|
+
* but TOTAL over the grammar (opt(named)/arr(arr)/map(opt)…) and inlined — the type is baked all the way
|
|
160
|
+
* through by the TS compiler, and every seam call resolves to a CONCRETE consumer type (no dispatch).
|
|
161
|
+
*/
|
|
162
|
+
function renderGoDeBox(plan, target, srcExpr, ctr, indent, fail, idxExpr = "") {
|
|
163
|
+
const I = indent;
|
|
164
|
+
const I1 = indent + "\t";
|
|
165
|
+
const I2 = indent + "\t\t";
|
|
166
|
+
const id = ctr.n++;
|
|
167
|
+
const ml = JSON.stringify(plan.model);
|
|
168
|
+
const fl = JSON.stringify(plan.field);
|
|
169
|
+
const nl = JSON.stringify(plan.expectedType);
|
|
170
|
+
const opt = plan.optDepth > 0;
|
|
171
|
+
const pv = `p${id}`;
|
|
172
|
+
const kindSuffix = plan.k === "list" ? "List" : plan.k === "scalar" ? (plan.scalar === "string" ? "String" : plan.scalar === "bool" ? "Bool" : "Number") : "Row";
|
|
173
|
+
// the probe call for this node's access (top → As*, field/mapval → Probe*(key), elem → Elem*(i)).
|
|
174
|
+
const probeCall = (() => {
|
|
175
|
+
switch (plan.access.from) {
|
|
176
|
+
case "top":
|
|
177
|
+
return `${srcExpr}.As${kindSuffix}()`;
|
|
178
|
+
case "field":
|
|
179
|
+
return `${srcExpr}.Probe${kindSuffix}(${JSON.stringify(plan.access.key)})`;
|
|
180
|
+
case "elem":
|
|
181
|
+
return `${srcExpr}.Elem${kindSuffix}(${idxExpr})`;
|
|
182
|
+
case "mapval":
|
|
183
|
+
return `${srcExpr}.Probe${kindSuffix}(${idxExpr})`;
|
|
148
184
|
}
|
|
185
|
+
})();
|
|
186
|
+
const mismatch = fail(`deTypeMismatch(${ml}, ${fl}, ${nl}, ${pv}.ActualWireType, ${pv}.Raw)`);
|
|
187
|
+
const missing = fail(`deMissingField(${ml}, ${fl}, ${nl})`);
|
|
188
|
+
// wrap a decoded BASE-typed temp in optDepth `&` layers into target (present ⇒ &^N; absent/null ⇒ nil).
|
|
189
|
+
const wrapAssign = (baseTemp) => {
|
|
190
|
+
if (plan.optDepth === 0)
|
|
191
|
+
return [`${I1}${target} = ${baseTemp}`];
|
|
192
|
+
const out = [];
|
|
193
|
+
let cur = baseTemp;
|
|
194
|
+
for (let i = 0; i < plan.optDepth; i++) {
|
|
195
|
+
if (i === plan.optDepth - 1)
|
|
196
|
+
out.push(`${I1}${target} = &${cur}`);
|
|
197
|
+
else {
|
|
198
|
+
const w = `w${id}_${i}`;
|
|
199
|
+
out.push(`${I1}${w} := &${cur}`);
|
|
200
|
+
cur = w;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
};
|
|
205
|
+
const lines = [];
|
|
206
|
+
lines.push(`${I}${pv} := ${probeCall}`);
|
|
207
|
+
lines.push(`${I}if ${pv}.Kind == probeGot {`);
|
|
208
|
+
if (plan.k === "scalar") {
|
|
209
|
+
if (plan.scalar === "string" || plan.scalar === "bool") {
|
|
210
|
+
if (plan.optDepth === 0)
|
|
211
|
+
lines.push(`${I1}${target} = ${pv}.Got`);
|
|
212
|
+
else {
|
|
213
|
+
lines.push(`${I1}sv${id} := ${pv}.Got`);
|
|
214
|
+
for (const l of wrapAssign(`sv${id}`))
|
|
215
|
+
lines.push(l);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// int/float: parse + range-check here (BC owns overflow) — no panic, a structured overflow error.
|
|
220
|
+
const parse = plan.scalar === "int" ? `strconv.ParseInt(${pv}.Got, 10, 64)` : `strconv.ParseFloat(${pv}.Got, 64)`;
|
|
221
|
+
lines.push(`${I1}n${id}, nErr${id} := ${parse}`);
|
|
222
|
+
lines.push(`${I1}if nErr${id} != nil {`);
|
|
223
|
+
lines.push(`${I2}${fail(`deOverflow(${ml}, ${fl}, ${nl}, ${pv}.ActualWireType, ${pv}.Got)`)}`);
|
|
224
|
+
lines.push(`${I1}}`);
|
|
225
|
+
for (const l of wrapAssign(`n${id}`))
|
|
226
|
+
lines.push(l);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (plan.k === "record") {
|
|
230
|
+
lines.push(`${I1}var rec${id} ${plan.typeName}`);
|
|
231
|
+
for (const f of plan.fields)
|
|
232
|
+
lines.push(renderGoDeBox(f.plan, `rec${id}.${goFieldName(f.name)}`, `${pv}.Got`, ctr, I1, fail));
|
|
233
|
+
for (const l of wrapAssign(`rec${id}`))
|
|
234
|
+
lines.push(l);
|
|
149
235
|
}
|
|
236
|
+
else if (plan.k === "list") {
|
|
237
|
+
const elemGo = goTypeOfPlan(plan.elem);
|
|
238
|
+
lines.push(`${I1}list${id} := make([]${elemGo}, 0, ${pv}.Got.Len())`);
|
|
239
|
+
lines.push(`${I1}for i${id} := 0; i${id} < ${pv}.Got.Len(); i${id}++ {`);
|
|
240
|
+
lines.push(`${I2}var el${id} ${elemGo}`);
|
|
241
|
+
lines.push(renderGoDeBox(plan.elem, `el${id}`, `${pv}.Got`, ctr, I2, fail, `i${id}`));
|
|
242
|
+
lines.push(`${I2}list${id} = append(list${id}, el${id})`);
|
|
243
|
+
lines.push(`${I1}}`);
|
|
244
|
+
for (const l of wrapAssign(`list${id}`))
|
|
245
|
+
lines.push(l);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// map(V): the wire value is a producer map; enumerate its keys, decode each value as V.
|
|
249
|
+
const valGo = goTypeOfPlan(plan.value);
|
|
250
|
+
lines.push(`${I1}m${id} := make(map[string]${valGo})`);
|
|
251
|
+
lines.push(`${I1}for _, k${id} := range ${pv}.Got.Keys() {`);
|
|
252
|
+
lines.push(`${I2}var mv${id} ${valGo}`);
|
|
253
|
+
lines.push(renderGoDeBox(plan.value, `mv${id}`, `${pv}.Got`, ctr, I2, fail, `k${id}`));
|
|
254
|
+
lines.push(`${I2}m${id}[k${id}] = mv${id}`);
|
|
255
|
+
lines.push(`${I1}}`);
|
|
256
|
+
for (const l of wrapAssign(`m${id}`))
|
|
257
|
+
lines.push(l);
|
|
258
|
+
}
|
|
259
|
+
// WRONG → mismatch (required + optional). Required: NULL → mismatch, ABSENT → missing. Optional:
|
|
260
|
+
// NULL / ABSENT leave the target pointer nil (no statement — the zero value IS None).
|
|
261
|
+
lines.push(`${I}} else if ${pv}.Kind == probeWrong {`);
|
|
262
|
+
lines.push(`${I1}${mismatch}`);
|
|
263
|
+
if (!opt) {
|
|
264
|
+
lines.push(`${I}} else if ${pv}.Kind == probeNull {`);
|
|
265
|
+
lines.push(`${I1}${mismatch}`);
|
|
266
|
+
lines.push(`${I}} else {`);
|
|
267
|
+
lines.push(`${I1}${missing}`);
|
|
268
|
+
}
|
|
269
|
+
lines.push(`${I}}`);
|
|
270
|
+
return lines.join("\n");
|
|
150
271
|
}
|
|
151
|
-
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
272
|
+
/** returnZeroFail — the default de-box mismatch control flow: return the runner's zero output + the
|
|
273
|
+
* structured Error Value (fail/retry componentRef results + map/fanout element de-box, which fails closed
|
|
274
|
+
* regardless of elementPolicy — matching the interpreter's whole-node outType throw). */
|
|
275
|
+
const returnZeroFail = (zeroOut) => (errValueExpr) => `return ${zeroOut}, ${errValueExpr}`;
|
|
276
|
+
/** renderResultDeBox — inline strict de-box of a whole result WIRE (`srcExpr`, a WireValue) into the
|
|
277
|
+
* lvalue `target`, from the TOTAL SSoT plan for `ref` at the top position. `fail` selects the mismatch
|
|
278
|
+
* control flow. This is the ONE de-box call the runner arms share (componentRef result + map/fanout
|
|
279
|
+
* element) — no per-position predicate, no helper decode fn. `ctr` hands out unique temp suffixes: the
|
|
280
|
+
* sequential runner threads ONE counter across all its at-fn-scope nodes so siblings never collide;
|
|
281
|
+
* loop/if-scoped arms (map / fanout / parallel commit) may pass a fresh one. */
|
|
282
|
+
function renderResultDeBox(ref, target, srcExpr, nodeId, plan, indent, fail, ctr = { n: 0 }) {
|
|
283
|
+
return renderGoDeBox(buildDeBoxPlan(ref, { from: "top" }, { model: nodeId, field: nodeId, plan }), target, srcExpr, ctr, indent, fail);
|
|
284
|
+
}
|
|
285
|
+
/** emitProbeWireTypes — the once-per-module probe kind consts + probe RESULT structs. A NumberProbe's
|
|
286
|
+
* Got is the raw numeric text (the de-box parses + range-checks it, so overflow is BC's to detect); every
|
|
287
|
+
* probe carries ActualWireType even on a match so overflow can report the numeric wire tag. Raw is the
|
|
288
|
+
* offending value stringified (NOT named RawValue — that token is reserved for the ErrorDetail field
|
|
289
|
+
* decl). Concrete data structs — no boxed Value on the seam. bc does NOT declare the WireValue/WireRow/
|
|
290
|
+
* WireList seam types (that would own the wire format, and a Go interface is vtable dynamic dispatch); it
|
|
291
|
+
* REFERENCES the consumer's CONCRETE types by name (RowProbe.Got is a `${wire.row}`, ListProbe.Got a
|
|
292
|
+
* `${wire.list}`) and the runner calls their methods directly (zero dynamic dispatch, zero dictionary). */
|
|
293
|
+
function emitProbeWireTypes(wire) {
|
|
157
294
|
return `// Probe kinds — the outcome of classifying one wire attribute against a declared field type.
|
|
158
295
|
const (
|
|
159
296
|
probeGot uint8 = 0 // present; the wire variant matches the declared type
|
|
@@ -187,232 +324,23 @@ type BoolProbe struct {
|
|
|
187
324
|
Raw string
|
|
188
325
|
}
|
|
189
326
|
|
|
190
|
-
// RowProbe / ListProbe — a composite probe result: a nested
|
|
327
|
+
// RowProbe / ListProbe — a composite probe result: a nested wire map (a DynamoDB "M") or wire array (an
|
|
328
|
+
// "L"). Got is the consumer's CONCRETE ${wire.row} / ${wire.list} (a value type — no boxed Value, no
|
|
329
|
+
// interface); the inline de-box probes it directly.
|
|
191
330
|
type RowProbe struct {
|
|
192
331
|
Kind uint8
|
|
193
|
-
Got
|
|
332
|
+
Got ${wire.row}
|
|
194
333
|
ActualWireType string
|
|
195
334
|
Raw string
|
|
196
335
|
}
|
|
197
336
|
|
|
198
337
|
type ListProbe struct {
|
|
199
338
|
Kind uint8
|
|
200
|
-
Got
|
|
339
|
+
Got ${wire.list}
|
|
201
340
|
ActualWireType string
|
|
202
341
|
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
342
|
}`;
|
|
228
343
|
}
|
|
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
344
|
/** mapFanoutElemRef — the element ROW TypeRef a map/fanout node decodes (the per-element record). */
|
|
417
345
|
function mapFanoutElemRef(node, typedNodes, plan) {
|
|
418
346
|
const ref = typedNodes.get(node.id);
|
|
@@ -420,190 +348,6 @@ function mapFanoutElemRef(node, typedNodes, plan) {
|
|
|
420
348
|
return fanoutItemsElemRef(node, ref, plan);
|
|
421
349
|
return mapElemRowRef(node, ref, plan);
|
|
422
350
|
}
|
|
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
|
-
}
|
|
607
351
|
function goPortFieldName(wire) {
|
|
608
352
|
const safe = wire.replace(/[^A-Za-z0-9_]/g, "_");
|
|
609
353
|
const head = safe.charAt(0);
|
|
@@ -1050,32 +794,11 @@ type ${structName} struct {
|
|
|
1050
794
|
${fields}
|
|
1051
795
|
}`;
|
|
1052
796
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
// per-component `Handler_<comp>` interface has one method per node returning that concrete struct;
|
|
1059
|
-
// the runner reads `row.<Field>` DIRECTLY (no `.(RawRow)`, no `.(scalar)`, no `RawValue`) and copies
|
|
1060
|
-
// each field into the node's outType struct local. The `.(T)` type-assert seam that the generic
|
|
1061
|
-
// RawValue de-box carried is GONE on the covered path (the handler produces the typed fields).
|
|
1062
|
-
//
|
|
1063
|
-
// A scalar/arr/opt node (outType not a named struct) has a single `Val <typed>` payload field. A
|
|
1064
|
-
// named-struct node has one Go field per outType field (same names/types as the outType decl), so
|
|
1065
|
-
// `RawRow_<comp>_<node>` is structurally the outType struct. The copier is then a
|
|
1066
|
-
// mechanical field-for-field assignment into the outType struct — NO dynamic value read anywhere.
|
|
1067
|
-
/** Row_<comp>_<node> — the concrete per-node handler-result row struct name. (Deliberately NOT
|
|
1068
|
-
* prefixed with the generic dynamic-accessor name — that accessor is what the concrete path removes.) */
|
|
1069
|
-
function rawRowStructName(compName, nodeId) {
|
|
1070
|
-
return `Row_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
1071
|
-
}
|
|
1072
|
-
/** RowElem_<comp>_<node> — the concrete per-ELEMENT row struct name for a covered map node (the
|
|
1073
|
-
* element/into row the map handler yields per over element). */
|
|
1074
|
-
function rawElemStructName(compName, nodeId) {
|
|
1075
|
-
return `RowElem_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
1076
|
-
}
|
|
1077
|
-
/** Handler_<comp> — the per-component concrete handler interface name (one Node_* method per node). */
|
|
1078
|
-
function handlerIfaceName(compName) {
|
|
797
|
+
/** handlerTypeName — the per-component CONCRETE handler type name the runner takes by value (one Node_*
|
|
798
|
+
* method per node). It is NOT a bc-declared interface (a Go interface is vtable dynamic dispatch): the
|
|
799
|
+
* consumer supplies a concrete type of this name (default `Handler_<comp>`) and the runner calls
|
|
800
|
+
* `h.Node_*(...)` directly (zero dynamic dispatch, zero dictionary). */
|
|
801
|
+
function handlerTypeName(compName) {
|
|
1079
802
|
return `Handler_${sanitize(compName)}`;
|
|
1080
803
|
}
|
|
1081
804
|
/** RunNativeRawStruct_<comp> — the EXPORTED name of the per-component struct-returning combined runner
|
|
@@ -1090,74 +813,6 @@ function runnerName(compName) {
|
|
|
1090
813
|
function nodeMethodName(compName, nodeId) {
|
|
1091
814
|
return `Node_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
1092
815
|
}
|
|
1093
|
-
/**
|
|
1094
|
-
* emitRawRowStructs — emit the concrete `RawRow_<comp>_<node>` struct per covered node (and, for a
|
|
1095
|
-
* covered map node, the per-element `RawElem_<comp>_<node>`). A named-struct outType flattens to one
|
|
1096
|
-
* Go field per outType field; a scalar/arr/opt node carries a single `Val` payload. Every row carries
|
|
1097
|
-
* A row models a SUCCESS; failure is the second (error) return, so no in-band error signal.
|
|
1098
|
-
*/
|
|
1099
|
-
function emitRawRowStructs(comp, typedNodes, plan) {
|
|
1100
|
-
const parts = [];
|
|
1101
|
-
for (const n of comp.body) {
|
|
1102
|
-
// #108: a cond node has NO handler-result row (pure Expression — no dispatch).
|
|
1103
|
-
if ("cond" in n)
|
|
1104
|
-
continue;
|
|
1105
|
-
const ref = typedNodes.get(n.id);
|
|
1106
|
-
if ("fanout" in n) {
|
|
1107
|
-
// a covered fanout node: RawElem_ is the connection items element (the per-body row the batched
|
|
1108
|
-
// handler yields, aligned to the deduped id list); the batch RawRow_ carries the aligned []RawElem_.
|
|
1109
|
-
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
1110
|
-
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
1111
|
-
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
|
|
1112
|
-
`type ${rawRowStructName(comp.name, n.id)} struct {\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
|
|
1113
|
-
continue;
|
|
1114
|
-
}
|
|
1115
|
-
if ("map" in n) {
|
|
1116
|
-
// a covered map node: RawElem_<comp>_<node> is the HANDLER'S per-element return shape:
|
|
1117
|
-
// - into: the `into` field's type (the fetched item merged onto every over element);
|
|
1118
|
-
// - no-into (#93 shape 3): the element outType directly (connection-collect).
|
|
1119
|
-
// The batched Node_* returns RawRow_<comp>_<node> (aligned []RawElem_ + error); a per-element
|
|
1120
|
-
// dispatch Node_* returns RawElem_ directly (one physical call per element).
|
|
1121
|
-
const arrRef = ref;
|
|
1122
|
-
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
1123
|
-
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
1124
|
-
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
|
|
1125
|
-
`type ${rawRowStructName(comp.name, n.id)} struct {\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
|
|
1126
|
-
continue;
|
|
1127
|
-
}
|
|
1128
|
-
parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
|
|
1129
|
-
}
|
|
1130
|
-
return parts.join("\n\n");
|
|
1131
|
-
}
|
|
1132
|
-
/** Emit one concrete row struct: flatten a named outType to typed fields, else a single Val payload;
|
|
1133
|
-
* A row models a SUCCESS; a failure is the second (error) return, so no in-band error signal. */
|
|
1134
|
-
function emitOneRowStruct(name, ref, plan) {
|
|
1135
|
-
const lines = [];
|
|
1136
|
-
lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType).`);
|
|
1137
|
-
lines.push(`type ${name} struct {`);
|
|
1138
|
-
if (ref.kind === "named") {
|
|
1139
|
-
const decl = findDecl(plan, ref.name);
|
|
1140
|
-
for (const f of decl.fields) {
|
|
1141
|
-
lines.push(`\t${goFieldName(f.name)} ${renderTypeRef(f.type)} // ${JSON.stringify(f.name)}`);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
else {
|
|
1145
|
-
lines.push(`\tVal ${renderTypeRef(ref)}`);
|
|
1146
|
-
}
|
|
1147
|
-
lines.push(`}`);
|
|
1148
|
-
return lines.join("\n");
|
|
1149
|
-
}
|
|
1150
|
-
/** emitRowCopy — the statements copying a concrete row's typed fields into the node's outType local
|
|
1151
|
-
* `target` (a value of Go type renderTypeRef(ref)). Named: field-for-field; scalar/arr/opt: `.Val`.
|
|
1152
|
-
* `rowExpr` is the concrete RawRow_/RawElem_ value; `indent` tabs. No dynamic read — pure assignment. */
|
|
1153
|
-
function emitRowCopy(target, rowExpr, ref, plan, indent) {
|
|
1154
|
-
const t = "\t".repeat(indent);
|
|
1155
|
-
if (ref.kind === "named") {
|
|
1156
|
-
const decl = findDecl(plan, ref.name);
|
|
1157
|
-
return decl.fields.map((f) => `${t}${target}.${goFieldName(f.name)} = ${rowExpr}.${goFieldName(f.name)}`).join("\n");
|
|
1158
|
-
}
|
|
1159
|
-
return `${t}${target} = ${rowExpr}.Val`;
|
|
1160
|
-
}
|
|
1161
816
|
/**
|
|
1162
817
|
* boundGoType (change #3 — typed `bound`) — the CONCRETE Go type of a covered node's `bound` handler
|
|
1163
818
|
* param (bc#90 / runtime-free). `bound` is the parent-produced key (the bindField value); it replaces
|
|
@@ -1187,58 +842,6 @@ function boundGoType(node, comp, typedNodes, plan) {
|
|
|
1187
842
|
return `*${renderTypeRef(bfRef.inner)}`;
|
|
1188
843
|
return `*${renderTypeRef(bfRef)}`;
|
|
1189
844
|
}
|
|
1190
|
-
/**
|
|
1191
|
-
* emitHandlerInterface — the per-component `Handler_<comp>` interface: one concrete-typed `Node_*`
|
|
1192
|
-
* method per covered node. Each componentRef node method takes the node's native ports struct + the
|
|
1193
|
-
* typed `bound` pointer (change #3 — the produced-aware parent key; NO boxed dslcontracts.Value) and
|
|
1194
|
-
* returns its concrete row struct + a resolved bool. A covered map node's method takes the batch
|
|
1195
|
-
* ports struct (batched) or the element ports struct (per-element) and returns the batch / element
|
|
1196
|
-
* row. This is the fully-concrete, runtime-free replacement for the generic ExecNativeRaw seam.
|
|
1197
|
-
*/
|
|
1198
|
-
function emitHandlerInterface(comp, typedNodes, plan) {
|
|
1199
|
-
const lines = [];
|
|
1200
|
-
lines.push(`// ${handlerIfaceName(comp.name)} — the CONCRETE per-component handler seam: one typed method`);
|
|
1201
|
-
lines.push(`// per covered node (native ports struct IN, concrete row struct OUT). No generic boxed-ports`);
|
|
1202
|
-
lines.push(`// / boxed-value / dynamic accessor crosses the covered boundary — the consumer implements each`);
|
|
1203
|
-
lines.push(`// Node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
|
|
1204
|
-
lines.push(`type ${handlerIfaceName(comp.name)} interface {`);
|
|
1205
|
-
for (const n of comp.body) {
|
|
1206
|
-
if ("cond" in n)
|
|
1207
|
-
continue; // #108: a cond node has no handler method (pure Expression).
|
|
1208
|
-
const method = nodeMethodName(comp.name, n.id);
|
|
1209
|
-
const bt = boundGoType(n, comp, typedNodes, plan);
|
|
1210
|
-
if ("fanout" in n) {
|
|
1211
|
-
// fanout (v3): one batched dispatch of the deduped id list. Ports = the batch struct; returns the
|
|
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.
|
|
1214
|
-
const portsT = `${portsStructName(comp.name, n.id)}_batch`;
|
|
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)`);
|
|
1218
|
-
continue;
|
|
1219
|
-
}
|
|
1220
|
-
if ("map" in n) {
|
|
1221
|
-
const m = n.map;
|
|
1222
|
-
const batched = m.batched === true;
|
|
1223
|
-
const portsT = batched ? `${portsStructName(comp.name, n.id)}_batch` : portsStructName(comp.name, n.id);
|
|
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)`);
|
|
1229
|
-
continue;
|
|
1230
|
-
}
|
|
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)`);
|
|
1238
|
-
}
|
|
1239
|
-
lines.push(`}`);
|
|
1240
|
-
return lines.join("\n");
|
|
1241
|
-
}
|
|
1242
845
|
/** mapElemMaterializerName — the augmented-element copier for a covered map...into node. */
|
|
1243
846
|
function mapElemMaterializerName(compName, nodeId) {
|
|
1244
847
|
return `materializeMapElemNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
@@ -1351,9 +954,8 @@ function mapElemRowRef(node, arrRef, plan) {
|
|
|
1351
954
|
}
|
|
1352
955
|
/**
|
|
1353
956
|
* emitMapElemMaterializers — for each covered map...into node, emit the AUGMENTED-element copier: copy
|
|
1354
|
-
* the over element's typed fields +
|
|
1355
|
-
*
|
|
1356
|
-
* produced the typed into row).
|
|
957
|
+
* the over element's typed fields + place the already-de-boxed `into` value into its field. Pure struct
|
|
958
|
+
* field assignment — NO dynamic value read (the inline de-box already produced the typed `into` value).
|
|
1357
959
|
*/
|
|
1358
960
|
function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
1359
961
|
const parts = [];
|
|
@@ -1375,16 +977,15 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
|
1375
977
|
const intoRowRef = mapElemRowRef(n, arrRef, plan);
|
|
1376
978
|
const fn = mapElemMaterializerName(comp.name, n.id);
|
|
1377
979
|
const overElemGo = mapOverElemType(comp, n, typedNodes, plan);
|
|
1378
|
-
const elemRowGo = rawElemStructName(comp.name, n.id);
|
|
1379
980
|
const lines = [];
|
|
1380
981
|
lines.push(`// ${fn} — assemble the augmented map element '${n.id}': copy the over element's typed`);
|
|
1381
|
-
lines.push(`// fields + set the '${intoKey}' (into) field from the
|
|
1382
|
-
lines.push(`func ${fn}(over ${overElemGo}, into ${
|
|
982
|
+
lines.push(`// fields + set the '${intoKey}' (into) field from the already-de-boxed into value.`);
|
|
983
|
+
lines.push(`func ${fn}(over ${overElemGo}, into ${renderTypeRef(intoRowRef)}) ${elemRef.name} {`);
|
|
1383
984
|
lines.push(`\tvar out ${elemRef.name}`);
|
|
1384
985
|
for (const f of overFields) {
|
|
1385
986
|
lines.push(`\tout.${goFieldName(f.name)} = over.${goFieldName(f.name)}`);
|
|
1386
987
|
}
|
|
1387
|
-
lines.push(
|
|
988
|
+
lines.push(`\tout.${goFieldName(intoKey)} = into`);
|
|
1388
989
|
lines.push(`\treturn out`);
|
|
1389
990
|
lines.push(`}`);
|
|
1390
991
|
parts.push(lines.join("\n"));
|
|
@@ -1634,7 +1235,7 @@ function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
|
|
|
1634
1235
|
*
|
|
1635
1236
|
* NO RunPlan / []OpSpec / planSpec walk: the staging, the bound, and which ops are parallel are baked.
|
|
1636
1237
|
*/
|
|
1637
|
-
function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags) {
|
|
1238
|
+
function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags, wire) {
|
|
1638
1239
|
const lines = [];
|
|
1639
1240
|
const ids = stage.map((i) => comp.body[i].id);
|
|
1640
1241
|
lines.push(`\t// ── bc#87 real-concurrency stage {${ids.map((x) => JSON.stringify(x)).join(", ")}} — static parallel`);
|
|
@@ -1671,16 +1272,9 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1671
1272
|
const node = comp.body[i];
|
|
1672
1273
|
const s = sanitize(node.id);
|
|
1673
1274
|
const structName = portsStructName(comp.name, comp.body[i].id);
|
|
1674
|
-
|
|
1675
|
-
//
|
|
1676
|
-
|
|
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
|
-
}
|
|
1275
|
+
// every member's goroutine returns the result WIRE (a single WireValue); the ascending COMMIT phase
|
|
1276
|
+
// de-boxes it INLINE (so the wire — not the decode — is what the goroutine writes to its slot).
|
|
1277
|
+
lines.push(`\t\tvar wire_${s} ${wire.value}`);
|
|
1684
1278
|
lines.push(`\t\tvar row_${s}Err error`);
|
|
1685
1279
|
lines.push(`\t\trun_${s} := false`);
|
|
1686
1280
|
lines.push(`\t\tvar ps_${s} ${structName}`);
|
|
@@ -1753,66 +1347,43 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1753
1347
|
lines.push(`\t\t\t\tdefer wg.Done()`);
|
|
1754
1348
|
lines.push(`\t\t\t\tdefer func() { <-sem }()`);
|
|
1755
1349
|
const boundArg = node.bindField !== undefined ? `bound_${s}` : "nil";
|
|
1756
|
-
|
|
1757
|
-
lines.push(`\t\t\t\t${resultVar}, row_${s}Err = handlers.${nodeMethodName(comp.name, node.id)}(ps_${s}, ${boundArg})`);
|
|
1350
|
+
lines.push(`\t\t\t\twire_${s}, row_${s}Err = handlers.${nodeMethodName(comp.name, node.id)}(ps_${s}, ${boundArg})`);
|
|
1758
1351
|
lines.push(`\t\t\t}()`);
|
|
1759
1352
|
lines.push(`\t\t}`);
|
|
1760
1353
|
}
|
|
1761
1354
|
lines.push(`\t\twg.Wait()`);
|
|
1762
|
-
// 3) COMMIT (ascending index): interpret +
|
|
1355
|
+
// 3) COMMIT (ascending index): interpret + INLINE de-box deterministically — lowest-index failure wins.
|
|
1356
|
+
// Uniform seam: every member's slot holds a single result WIRE (WireValue); the inline de-box
|
|
1357
|
+
// (renderResultDeBox) classifies it against the STATICALLY declared result type and produces the
|
|
1358
|
+
// structured Error Value on a mismatch (byte-equal codes to run_behavior's outType check, so ≡ holds).
|
|
1763
1359
|
for (const i of refMembers) {
|
|
1764
1360
|
const node = comp.body[i];
|
|
1765
1361
|
const meta = metas[i];
|
|
1766
1362
|
const s = sanitize(node.id);
|
|
1767
1363
|
const ref = typedNodes.get(node.id);
|
|
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.
|
|
1774
1364
|
lines.push(`\t\tif run_${s} {`);
|
|
1775
1365
|
if (meta.policy === "continue") {
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 4));
|
|
1791
|
-
lines.push(`\t\t\t\tproduced_${s} = true`);
|
|
1792
|
-
}
|
|
1366
|
+
// continue: a failed member (leaf failure OR de-box mismatch) commits no Port (defensive — covered
|
|
1367
|
+
// componentRef members are always 'fail' policy today). A local skip, NOT a runner-wide propagate.
|
|
1368
|
+
const nodeGo = renderTypeRef(ref);
|
|
1369
|
+
const nodeZeroFail = (e) => `return ${goZero(ref)}, ${e}`;
|
|
1370
|
+
lines.push(`\t\t\tif row_${s}Err == nil {`);
|
|
1371
|
+
lines.push(`\t\t\t\tdec_${s}, dec_${s}DErr := func() (${nodeGo}, error) {`);
|
|
1372
|
+
lines.push(`\t\t\t\t\tvar out ${nodeGo}`);
|
|
1373
|
+
lines.push(renderResultDeBox(ref, "out", `wire_${s}`, node.id, plan, "\t\t\t\t\t", nodeZeroFail));
|
|
1374
|
+
lines.push(`\t\t\t\t\treturn out, nil`);
|
|
1375
|
+
lines.push(`\t\t\t\t}()`);
|
|
1376
|
+
lines.push(`\t\t\t\tif dec_${s}DErr == nil {`);
|
|
1377
|
+
lines.push(`\t\t\t\t\t${typedLocal(node.id)} = dec_${s}`);
|
|
1378
|
+
lines.push(`\t\t\t\t\tproduced_${s} = true`);
|
|
1379
|
+
lines.push(`\t\t\t\t}`);
|
|
1793
1380
|
lines.push(`\t\t\t}`);
|
|
1794
1381
|
}
|
|
1795
1382
|
else {
|
|
1796
|
-
lines.push(`\t\t\tif ${
|
|
1797
|
-
lines.push(`\t\t\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, ${JSON.stringify(meta.policy === "retry" ? "retry" : "fail")}, ${
|
|
1383
|
+
lines.push(`\t\t\tif row_${s}Err != nil {`);
|
|
1384
|
+
lines.push(`\t\t\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, ${JSON.stringify(meta.policy === "retry" ? "retry" : "fail")}, row_${s}Err)`);
|
|
1798
1385
|
lines.push(`\t\t\t}`);
|
|
1799
|
-
|
|
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
|
-
}
|
|
1386
|
+
lines.push(renderResultDeBox(ref, typedLocal(node.id), `wire_${s}`, node.id, plan, "\t\t\t", returnZeroFail(zeroOut)));
|
|
1816
1387
|
lines.push(`\t\t\tproduced_${s} = true`);
|
|
1817
1388
|
}
|
|
1818
1389
|
lines.push(`\t\t}`);
|
|
@@ -1833,7 +1404,7 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1833
1404
|
* consumer (graphddb) drives it and keeps the struct native. A Value observation is produced ONLY
|
|
1834
1405
|
* by the separate serialize wrapper (run_native_raw_<comp>, below), the single outer Value boundary.
|
|
1835
1406
|
*/
|
|
1836
|
-
function emitNativeRawRunner(comp, plan, flags) {
|
|
1407
|
+
function emitNativeRawRunner(comp, plan, flags, wire) {
|
|
1837
1408
|
const ep = execPlan(comp);
|
|
1838
1409
|
const order = ep.order;
|
|
1839
1410
|
const cplan = buildComponentPlan(comp, goStraightlineInternals.dialect, "scope");
|
|
@@ -1854,6 +1425,9 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1854
1425
|
const outRef = comp.outputType !== undefined ? deriveTypeRef(comp.outputType, plan) : undefined;
|
|
1855
1426
|
const outStructType = outRef ? renderTypeRef(outRef) : `${PKG}.Value`;
|
|
1856
1427
|
const zeroOut = outRef ? goZero(outRef) : "nil";
|
|
1428
|
+
// ONE de-box temp counter across all at-fn-scope sequential nodes so sibling reads never collide on a
|
|
1429
|
+
// temp name (a gated / loop-scoped arm has its own Go scope, so it may keep a fresh counter).
|
|
1430
|
+
const seqCtr = { n: 0 };
|
|
1857
1431
|
const lines = [];
|
|
1858
1432
|
lines.push(`// ${runnerName(comp.name)} — the STRUCT-RETURNING combined read (bc#77/#87): the fully`);
|
|
1859
1433
|
lines.push(`// de-plumbed path. Ports are a native struct (direct field assignment); the handler`);
|
|
@@ -1867,11 +1441,12 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1867
1441
|
lines.push(`// index order so the observed value / op multiset / failure precedence byte-match run_behavior`);
|
|
1868
1442
|
lines.push(`// (the goroutine spawn is the ONLY runtime element — the WHAT is baked). The output is a typed`);
|
|
1869
1443
|
lines.push(`// struct/value assembled by struct literal + field access — the consumer keeps it native.`);
|
|
1870
|
-
// FULLY CONCRETE
|
|
1871
|
-
//
|
|
1872
|
-
//
|
|
1873
|
-
//
|
|
1874
|
-
|
|
1444
|
+
// FULLY CONCRETE + NON-GENERIC: the runner takes the consumer's CONCRETE ${handlerTypeName(comp.name)}
|
|
1445
|
+
// handler by value and calls h.Node_*(...) DIRECTLY (a Go generic method call goes through a dictionary,
|
|
1446
|
+
// a Go interface through a vtable — BOTH are dynamic dispatch, so the covered plane uses neither). Each
|
|
1447
|
+
// Node_* returns the node's result WIRE (a single ${wire.value}); the runner de-boxes it INLINE
|
|
1448
|
+
// (fully-unrolled probe/match — no decode helper, no output recursion) into the node's outType struct.
|
|
1449
|
+
lines.push(`func ${runnerName(comp.name)}(handlers ${handlerTypeName(comp.name)}, in ${inStructName(comp.name)}) (${outStructType}, error) {`);
|
|
1875
1450
|
lines.push(`\t_ = in`);
|
|
1876
1451
|
for (const k of order.keys()) {
|
|
1877
1452
|
const node = comp.body[order[k]];
|
|
@@ -1895,7 +1470,7 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1895
1470
|
// ascending-sorted stages), so we emit the block at the first member and skip the rest.
|
|
1896
1471
|
const stage = ep.parallelStageOf.get(i);
|
|
1897
1472
|
if (stage !== undefined && stage[0] === i) {
|
|
1898
|
-
lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags));
|
|
1473
|
+
lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags, wire));
|
|
1899
1474
|
k += stage.length - 1;
|
|
1900
1475
|
continue;
|
|
1901
1476
|
}
|
|
@@ -1971,76 +1546,36 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1971
1546
|
inits.push(emitPortInit(pn, node.ports[pn], comp, plan, typedNodes, (h) => priorNodeAt(h, k)));
|
|
1972
1547
|
}
|
|
1973
1548
|
lines.push(`${indent}ports_${s} := ${structName}{${inits.join(", ")}}`);
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
//
|
|
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;
|
|
1549
|
+
// uniform seam: the handler returns a single result WIRE (${wire.value}); the inline de-box
|
|
1550
|
+
// (renderResultDeBox) classifies it against the STATICALLY declared result type and produces the
|
|
1551
|
+
// structured Error Value on a mismatch (byte-equal codes to run_behavior's outType check, so ≡ holds).
|
|
1984
1552
|
if (meta.policy === "continue") {
|
|
1985
|
-
// continue: a failed node (leaf failure OR de-box mismatch) produces no Port — the downstream
|
|
1986
|
-
//
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
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
|
-
}
|
|
1553
|
+
// continue: a failed node (leaf failure OR de-box mismatch) produces no Port — the downstream Skip
|
|
1554
|
+
// propagation is what `continue` means. The inline de-box runs in a local closure so a mismatch is a
|
|
1555
|
+
// LOCAL skip (not a runner-wide propagate); only a clean decode commits. (Covered componentRef nodes
|
|
1556
|
+
// are always 'fail' policy today — this arm is defensive.)
|
|
1557
|
+
const nodeGo = renderTypeRef(ref);
|
|
1558
|
+
const nodeZeroFail = (e) => `return ${goZero(ref)}, ${e}`;
|
|
1559
|
+
lines.push(`${indent}if wire_${s}, wire_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg}); wire_${s}Err == nil {`);
|
|
1560
|
+
lines.push(`${indent}\tdec_${s}, dec_${s}DErr := func() (${nodeGo}, error) {`);
|
|
1561
|
+
lines.push(`${indent}\t\tvar out ${nodeGo}`);
|
|
1562
|
+
lines.push(renderResultDeBox(ref, "out", `wire_${s}`, node.id, plan, `${indent}\t\t`, nodeZeroFail, seqCtr));
|
|
1563
|
+
lines.push(`${indent}\t\treturn out, nil`);
|
|
1564
|
+
lines.push(`${indent}\t}()`);
|
|
1565
|
+
lines.push(`${indent}\tif dec_${s}DErr == nil {`);
|
|
1566
|
+
lines.push(`${indent}\t\t${typedLocal(node.id)} = dec_${s}`);
|
|
1567
|
+
lines.push(`${indent}\t\tproduced_${s} = true`);
|
|
1568
|
+
lines.push(`${indent}\t}`);
|
|
1569
|
+
lines.push(`${indent}}`);
|
|
2009
1570
|
}
|
|
2010
1571
|
else {
|
|
2011
1572
|
const policyLit = JSON.stringify(meta.policy === "retry" ? "retry" : "fail");
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
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
|
-
}
|
|
1573
|
+
lines.push(`${indent}wire_${s}, wire_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg})`);
|
|
1574
|
+
lines.push(`${indent}if wire_${s}Err != nil {`);
|
|
1575
|
+
lines.push(`${indent}\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, ${policyLit}, wire_${s}Err)`);
|
|
1576
|
+
lines.push(`${indent}}`);
|
|
1577
|
+
lines.push(renderResultDeBox(ref, typedLocal(node.id), `wire_${s}`, node.id, plan, indent, returnZeroFail(zeroOut), seqCtr));
|
|
1578
|
+
lines.push(`${indent}produced_${s} = true`);
|
|
2044
1579
|
}
|
|
2045
1580
|
for (const c of closers)
|
|
2046
1581
|
lines.push(c);
|
|
@@ -2138,13 +1673,13 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
2138
1673
|
// over collection (typed field access to the parent node's arr — no serialize).
|
|
2139
1674
|
lines.push(`${indent}over_${s} := ${overExpr}`);
|
|
2140
1675
|
lines.push(`${indent}${typedLocal(node.id)} = make(${elemGo}, 0, len(over_${s}))`);
|
|
1676
|
+
// each element is de-boxed strictly INLINE: the handler returns the per-element WIRE and the arm decodes
|
|
1677
|
+
// it against the element ref (the `into` field's type for map...into, else the element type). A de-box
|
|
1678
|
+
// mismatch fails closed regardless of elementPolicy (matching the interpreter's whole-node outType
|
|
1679
|
+
// throw); elementPolicy still governs the handler's own leaf failures only.
|
|
2141
1680
|
const elemRowRef = mapElemRowRef(node, arrRef, plan);
|
|
2142
|
-
|
|
2143
|
-
|
|
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) : "";
|
|
1681
|
+
const elemRowGo = renderTypeRef(elemRowRef);
|
|
1682
|
+
const renderElemDeBox = (target, src, il) => renderResultDeBox(elemRowRef, target, src, node.id, plan, il, returnZeroFail(zeroOut));
|
|
2148
1683
|
// emit the per-element guard keep-gate (#108). `il` = indent, `elemVar` = the over element loop var.
|
|
2149
1684
|
// On skip: batched → `continue` (element excluded from items + keptOver); non-batched → `continue`.
|
|
2150
1685
|
const emitGuard = (il, elemVar) => {
|
|
@@ -2161,9 +1696,9 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
2161
1696
|
};
|
|
2162
1697
|
if (batched) {
|
|
2163
1698
|
// BATCHED (#86 pt2 into / #93 shape 3 no-into / #108 guarded no-into): build kept-element ports,
|
|
2164
|
-
// dispatch ONCE. The concrete Node_* returns
|
|
1699
|
+
// dispatch ONCE. The concrete Node_* returns the aligned per-element WIRES ([]WireValue) or an error.
|
|
2165
1700
|
// With a guard, `items` (and, for into, `keptOver`) hold only kept elements — matching run_behavior's
|
|
2166
|
-
// keptIdx-aligned batch. `into` is never combined with a guard (rejected).
|
|
1701
|
+
// keptIdx-aligned batch. `into` is never combined with a guard (rejected). Each wire is de-boxed inline.
|
|
2167
1702
|
lines.push(`${indent}items_${s} := make([]${structName}, 0, len(over_${s}))`);
|
|
2168
1703
|
if (hasInto)
|
|
2169
1704
|
lines.push(`${indent}keptOver_${s} := make([]${overElemGo}, 0, len(over_${s}))`);
|
|
@@ -2181,37 +1716,22 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
2181
1716
|
lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: items_${s}}`);
|
|
2182
1717
|
lines.push(`${indent}\tmo_${s}, mo_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
|
|
2183
1718
|
lines.push(`${indent}\tif mo_${s}Err != nil {`, `${indent}\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, "fail", mo_${s}Err)`, `${indent}\t}`);
|
|
2184
|
-
|
|
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}`);
|
|
1719
|
+
lines.push(`${indent}\tif len(mo_${s}) != 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}`);
|
|
2189
1720
|
if (hasInto) {
|
|
2190
|
-
// #86 pt2:
|
|
1721
|
+
// #86 pt2: de-box each aligned element wire (into the `into` field's type), then zip-attach onto the
|
|
1722
|
+
// kept over element (augmented element copier).
|
|
2191
1723
|
lines.push(`${indent}\tfor mk_${s} := range keptOver_${s} {`);
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
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
|
-
}
|
|
1724
|
+
lines.push(`${indent}\t\tvar dec_${s} ${elemRowGo}`);
|
|
1725
|
+
lines.push(renderElemDeBox(`dec_${s}`, `mo_${s}[mk_${s}]`, `${indent}\t\t`));
|
|
1726
|
+
lines.push(`${indent}\t\tel_${s} := ${elemMat}(keptOver_${s}[mk_${s}], dec_${s})`);
|
|
2200
1727
|
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
2201
1728
|
lines.push(`${indent}\t}`);
|
|
2202
1729
|
}
|
|
2203
1730
|
else {
|
|
2204
|
-
// #93 shape 3 / #108:
|
|
2205
|
-
lines.push(`${indent}\tfor mk_${s} := 0; mk_${s} < ${
|
|
1731
|
+
// #93 shape 3 / #108: de-box each aligned per-element wire DIRECTLY into the element (result = kept).
|
|
1732
|
+
lines.push(`${indent}\tfor mk_${s} := 0; mk_${s} < len(mo_${s}); mk_${s}++ {`);
|
|
2206
1733
|
lines.push(`${indent}\t\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
|
|
2207
|
-
|
|
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
|
-
}
|
|
1734
|
+
lines.push(renderElemDeBox(`el_${s}`, `mo_${s}[mk_${s}]`, `${indent}\t\t`));
|
|
2215
1735
|
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
2216
1736
|
lines.push(`${indent}\t}`);
|
|
2217
1737
|
}
|
|
@@ -2220,7 +1740,7 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
2220
1740
|
else {
|
|
2221
1741
|
// #93 shape 2 / #108: NON-batched map — dispatch the child handler ONCE PER KEPT ELEMENT (N physical
|
|
2222
1742
|
// requests, matching run_behavior's per-element loop). A guarded element that fails `when` is skipped
|
|
2223
|
-
// (no dispatch, excluded from the result). The concrete Node_* returns ONE
|
|
1743
|
+
// (no dispatch, excluded from the result). The concrete Node_* returns ONE element WIRE (WireValue).
|
|
2224
1744
|
lines.push(`${indent}for mk_${s} := range over_${s} {`);
|
|
2225
1745
|
lines.push(`${indent}\t${elemLocal(s)} := over_${s}[mk_${s}]`);
|
|
2226
1746
|
for (const l of emitGuard(indent + "\t", elemLocal(s)))
|
|
@@ -2240,20 +1760,17 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
2240
1760
|
else {
|
|
2241
1761
|
lines.push(`${indent}\tif mo_${s}Err != nil {`, `${indent}\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, "fail", mo_${s}Err)`, `${indent}\t}`);
|
|
2242
1762
|
}
|
|
2243
|
-
//
|
|
1763
|
+
// the element's wire is de-boxed strictly INLINE. A de-box mismatch fails closed regardless of
|
|
2244
1764
|
// elementPolicy (≡ the interpreter's whole-node outType throw); elementPolicy governs the handler's
|
|
2245
1765
|
// 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
|
-
}
|
|
2251
1766
|
if (hasInto) {
|
|
2252
|
-
lines.push(`${indent}\
|
|
1767
|
+
lines.push(`${indent}\tvar dec_${s} ${elemRowGo}`);
|
|
1768
|
+
lines.push(renderElemDeBox(`dec_${s}`, `mo_${s}`, `${indent}\t`));
|
|
1769
|
+
lines.push(`${indent}\tel_${s} := ${elemMat}(${elemLocal(s)}, dec_${s})`);
|
|
2253
1770
|
}
|
|
2254
1771
|
else {
|
|
2255
1772
|
lines.push(`${indent}\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
|
|
2256
|
-
lines.push(
|
|
1773
|
+
lines.push(renderElemDeBox(`el_${s}`, `mo_${s}`, `${indent}\t`));
|
|
2257
1774
|
}
|
|
2258
1775
|
lines.push(`${indent}\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
2259
1776
|
lines.push(`${indent}}`);
|
|
@@ -2324,10 +1841,9 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
|
|
|
2324
1841
|
// empty id-list => empty connection (handler not called), matching run_behavior.
|
|
2325
1842
|
lines.push(`${indent}if len(pi_${s}) > 0 {`);
|
|
2326
1843
|
lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: pi_${s}}`);
|
|
2327
|
-
// the
|
|
2328
|
-
//
|
|
2329
|
-
//
|
|
2330
|
-
const decElemFn = goDecodeElemName(comp.name, node.id);
|
|
1844
|
+
// the handler returns the aligned per-id element WIRES (one nilable WireValue per id); each present
|
|
1845
|
+
// body is de-boxed strictly INLINE (a de-box mismatch fails closed). A DANGLING id is a nil wire → the
|
|
1846
|
+
// zero element (empty dedupeKey), which the dangling drop below excludes.
|
|
2331
1847
|
lines.push(`${indent}\tfo_${s}, fo_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
|
|
2332
1848
|
lines.push(`${indent}\tif fo_${s}Err != nil {`, `${indent}\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, "fail", fo_${s}Err)`, `${indent}\t}`);
|
|
2333
1849
|
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}`);
|
|
@@ -2339,9 +1855,7 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
|
|
|
2339
1855
|
lines.push(`${indent}\tfor fi_${s} := range fo_${s} {`);
|
|
2340
1856
|
lines.push(`${indent}\t\tvar el_${s} ${elemGo}`);
|
|
2341
1857
|
lines.push(`${indent}\t\tif fo_${s}[fi_${s}] != nil {`);
|
|
2342
|
-
lines.push(
|
|
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));
|
|
1858
|
+
lines.push(renderResultDeBox(elemRef, `el_${s}`, `(*fo_${s}[fi_${s}])`, node.id, plan, `${indent}\t\t\t`, returnZeroFail(zeroOut)));
|
|
2345
1859
|
lines.push(`${indent}\t\t}`);
|
|
2346
1860
|
lines.push(`${indent}\t\tkey_${s} := el_${s}.${dkGo}`);
|
|
2347
1861
|
if (f.drop === "dangling") {
|
|
@@ -2787,7 +2301,7 @@ var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name
|
|
|
2787
2301
|
* mismatch Sprintf, `strings` for a concat builder, `sync` for a parallel stage). ExpectedSpecVersions
|
|
2788
2302
|
* stays as a local `var` for provenance, but the old `dslcontracts.SpecVersions` skew `init()` is GONE
|
|
2789
2303
|
* (change #5 — there is no runtime to skew against once the module is runtime-free). */
|
|
2790
|
-
function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath, needStrconv = false) {
|
|
2304
|
+
function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath, needStrconv = false, wireImport) {
|
|
2791
2305
|
const sv = ctx.specVersions;
|
|
2792
2306
|
const imports = [];
|
|
2793
2307
|
if (needFmt)
|
|
@@ -2800,6 +2314,10 @@ function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath, needSt
|
|
|
2800
2314
|
imports.push(`\t"strings"`);
|
|
2801
2315
|
if (needSync)
|
|
2802
2316
|
imports.push(`\t"sync"`);
|
|
2317
|
+
// a configured consumer wire package (goWire.import) — the covered module references its CONCRETE
|
|
2318
|
+
// WireValue/WireRow/WireList types (default: in-package, no import).
|
|
2319
|
+
if (wireImport !== undefined)
|
|
2320
|
+
imports.push(`\t${JSON.stringify(wireImport)}`);
|
|
2803
2321
|
const importBlock = imports.length > 0 ? `\nimport (\n${imports.join("\n")}\n)\n` : "";
|
|
2804
2322
|
return `// GENERATED by behavior-contracts (bc#90, go-typed-native — the RUNTIME-FREE covered read de-box) — DO NOT EDIT.
|
|
2805
2323
|
// Input: portable component-graph IR only. Every component here is a COVERED READ (sequential typed
|
|
@@ -2825,17 +2343,16 @@ function emit(ctx) {
|
|
|
2825
2343
|
// #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler when it emits
|
|
2826
2344
|
// a fallible checked-arith helper call — determines whether the helper library + `math` import ship).
|
|
2827
2345
|
goExprUsed.helpers = false;
|
|
2346
|
+
const wire = resolveGoWire(ctx);
|
|
2828
2347
|
// Emit the runners first so the nv* helper usage flags are known.
|
|
2829
2348
|
const flags = { bind: false, field: false, cat: false, arr: false };
|
|
2830
2349
|
const structs = [];
|
|
2831
2350
|
const inStructs = [];
|
|
2832
|
-
const rowStructs = [];
|
|
2833
|
-
const handlerIfaces = [];
|
|
2834
2351
|
const elemCopiers = [];
|
|
2835
2352
|
const runnerCodes = [];
|
|
2836
|
-
// the
|
|
2837
|
-
|
|
2838
|
-
const
|
|
2353
|
+
// every covered node returns a WireValue the inline de-box classifies — the wire seam (probe kind
|
|
2354
|
+
// consts + probe RESULT structs) is emitted once whenever any handler node exists.
|
|
2355
|
+
const anyWire = native.some((c) => c.body.some((n) => !("cond" in n)));
|
|
2839
2356
|
for (const c of native) {
|
|
2840
2357
|
inStructs.push(emitInStruct(c, plan));
|
|
2841
2358
|
const typedNodes = new Map();
|
|
@@ -2861,18 +2378,11 @@ function emit(ctx) {
|
|
|
2861
2378
|
structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes));
|
|
2862
2379
|
}
|
|
2863
2380
|
}
|
|
2864
|
-
//
|
|
2865
|
-
assertDeBoxableResults(c, typedNodes, plan);
|
|
2866
|
-
// CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
|
|
2867
|
-
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
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);
|
|
2871
|
-
// augmented-element copiers for covered map...into nodes (over fields + concrete into row).
|
|
2381
|
+
// augmented-element copiers for covered map...into nodes (over fields + the de-boxed into value).
|
|
2872
2382
|
const mm = emitMapElemMaterializers(c, typedNodes, plan);
|
|
2873
2383
|
if (mm)
|
|
2874
2384
|
elemCopiers.push(mm);
|
|
2875
|
-
const r = emitNativeRawRunner(c, plan, flags);
|
|
2385
|
+
const r = emitNativeRawRunner(c, plan, flags, wire);
|
|
2876
2386
|
runnerCodes.push(r.code);
|
|
2877
2387
|
}
|
|
2878
2388
|
const structSurface = emitStructSurface(native);
|
|
@@ -2901,37 +2411,31 @@ function emit(ctx) {
|
|
|
2901
2411
|
// #108: `math` is imported iff the native-expr compiler emitted a fallible helper (bcExpr_modF uses
|
|
2902
2412
|
// math.Mod). The helper library itself is baked into the module only when used (goExprUsed.helpers).
|
|
2903
2413
|
const needMath = goExprUsed.helpers;
|
|
2904
|
-
// `strconv` is imported iff a generated de-box parses a numeric field (int/float range check).
|
|
2905
|
-
const needStrconv =
|
|
2906
|
-
const header = emitMinimalHeader(ctx, false, needSync, needFmt, needMath, needStrconv);
|
|
2414
|
+
// `strconv` is imported iff a generated inline de-box parses a numeric field (int/float range check).
|
|
2415
|
+
const needStrconv = runnerCodes.some((c) => c.includes("strconv."));
|
|
2416
|
+
const header = emitMinimalHeader(ctx, false, needSync, needFmt, needMath, needStrconv, wire.import);
|
|
2907
2417
|
const banner = `// COMBINED READ layer (bc#90 — the RUNTIME-FREE read de-box; STRUCT-ONLY, FULLY-NATIVE surface). A
|
|
2908
2418
|
// covered read is a strictly-sequential typed componentRef chain (point reads + relationKind:single
|
|
2909
2419
|
// children), emitted ONLY here: each node builds a native ports struct by direct field assignment (every
|
|
2910
2420
|
// port is a CONCRETE Go value — string / bool / []string projection / int64 limit — NO boxed Value, NO
|
|
2911
|
-
// PortReader accessor); the
|
|
2912
|
-
//
|
|
2913
|
-
//
|
|
2914
|
-
//
|
|
2915
|
-
//
|
|
2916
|
-
//
|
|
2421
|
+
// PortReader accessor); the runner takes the consumer's CONCRETE Handler_<comp> handler by
|
|
2422
|
+
// value and calls h.Node_*(...) DIRECTLY (no interface, no generics — zero dynamic dispatch). Each Node_*
|
|
2423
|
+
// takes a typed produced-aware 'bound' pointer and returns the node's result WIRE (a single ${wire.value});
|
|
2424
|
+
// the runner de-boxes it INLINE (fully-unrolled probe/match — no decode helper, no output recursion) into
|
|
2425
|
+
// the node's outType struct. The covered plane carries NO bc-runtime reference at all — failures use the
|
|
2426
|
+
// LOCAL *BehaviorError (same codes, byte-equal to run_behavior). Exec is INLINE sequential (no plan driver)
|
|
2427
|
+
// so a relation child reads the parent's REAL struct result via direct field access and the child-present
|
|
2917
2428
|
// decision is made from the real parent value — relationSingle / connection converge with run_behavior
|
|
2918
|
-
// (fixes #323/#74). The exposed API is the STRUCT-returning RunNativeRawStruct_<comp> (the consumer
|
|
2919
|
-
//
|
|
2920
|
-
//
|
|
2429
|
+
// (fixes #323/#74). The exposed API is the STRUCT-returning RunNativeRawStruct_<comp> (the consumer keeps
|
|
2430
|
+
// the model native — no serialization on the read hot path). This module is FULLY NATIVE: a naive grep for
|
|
2431
|
+
// boxing primitives OR for the bc-runtime package finds nothing, by design (bc#90/runtime-free).`;
|
|
2921
2432
|
const sections = [
|
|
2922
2433
|
banner,
|
|
2923
2434
|
`// Local concrete failure type (runtime-free) — a covered runner returns *BehaviorError (satisfies\n// error) instead of a bc-runtime failure, so the fully-covered module imports ZERO bc runtime.\n${behaviorErrorType}`,
|
|
2924
2435
|
decls ? `// Typed struct declarations (outType-derived; hash-dedup — shared type plan).\n${decls}` : undefined,
|
|
2925
2436
|
`// Per-component CONCRETE input structs (fields = inputPorts; the consumer builds them natively —\n// no generic *Obj crosses the covered read boundary).\n${inStructs.join("\n\n")}`,
|
|
2926
|
-
|
|
2927
|
-
? `//
|
|
2928
|
-
: undefined,
|
|
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")}`
|
|
2437
|
+
anyWire
|
|
2438
|
+
? `// strict de-box wire seam — the consumer supplies CONCRETE ${wire.value} / ${wire.row} / ${wire.list}\n// types (variant classification) and the runner classifies each node result against the STATICALLY\n// declared type via their methods; the generated INLINE de-box owns strictness (required/optional,\n// present/absent, error assembly, fail-closed). bc emits only the probe RESULT structs — concrete data,\n// no boxed Value, no interface, no dictionary crosses the seam.\n${emitProbeWireTypes(wire)}`
|
|
2935
2439
|
: undefined,
|
|
2936
2440
|
elemCopiers.length
|
|
2937
2441
|
? `// Augmented map-element copiers (over element fields + the concrete into row — pure assignment).\n${elemCopiers.join("\n\n")}`
|
|
@@ -2972,32 +2476,21 @@ export function goTypedNativeObserve(ir, runtimeImport) {
|
|
|
2972
2476
|
// decode the generic *Obj input into the CONCRETE In_<comp> struct (test glue — ONE decode at the
|
|
2973
2477
|
// top, OUTSIDE the covered hot path; a REQUIRED declared input is present so an absent key is a
|
|
2974
2478
|
// fail-closed decode error surfaced here, never on the native runner path).
|
|
2975
|
-
return `\tif name == ${JSON.stringify(c.name)} {\n\t\tin, ierr := decode_${inStructName(c.name)}(input)\n\t\tif ierr != nil {\n\t\t\treturn nil, ierr\n\t\t}\n\t\ttv, err := ${runnerName(c.name)}(handlers, in)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn ${ser}, nil\n\t}`;
|
|
2479
|
+
return `\tif name == ${JSON.stringify(c.name)} {\n\t\tin, ierr := decode_${inStructName(c.name)}(input)\n\t\tif ierr != nil {\n\t\t\treturn nil, ierr\n\t\t}\n\t\ttv, err := ${runnerName(c.name)}(${handlerTypeName(c.name)}{handlers}, in)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn ${ser}, nil\n\t}`;
|
|
2976
2480
|
})
|
|
2977
2481
|
.join("\n");
|
|
2978
2482
|
// input decoders (TEST glue): generic *Obj -> concrete In_<comp>. Scalar fields read the typed value
|
|
2979
2483
|
// (a `.(T)` here is on the SDK/wire Value in the OBSERVE companion, never in the covered module).
|
|
2980
2484
|
const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
|
|
2981
|
-
//
|
|
2982
|
-
//
|
|
2983
|
-
const anyDeBox = native.some((c) =>
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
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.
|
|
2485
|
+
// every covered node returns a WireValue the inline de-box classifies — so the scripted wire adapter is
|
|
2486
|
+
// needed whenever any component has a handler node.
|
|
2487
|
+
const anyDeBox = native.some((c) => c.body.some((n) => !("cond" in n)));
|
|
2488
|
+
// scripted wire adapter (TEST glue): wraps a scripted bc Value as the CONCRETE WireValue / WireRow /
|
|
2489
|
+
// WireList (the in-package default seam types the covered module references) and classifies each datum's
|
|
2490
|
+
// native type into a DynamoDB-flavored wire tag (S/N/BOOL/L/M/NULL). A real consumer supplies its OWN
|
|
2491
|
+
// concrete WireValue over its AttributeValue payload; this exists only so the harness drives the
|
|
2492
|
+
// generated inline de-box from the same scripted vectors run_behavior uses — so actualWireType observed
|
|
2493
|
+
// in a mismatch is the PRODUCER tag (e.g. "N"), not the collapsed native type.
|
|
3001
2494
|
const wireAdapter = anyDeBox
|
|
3002
2495
|
? `// ── scripted wire adapter (TEST glue) ──
|
|
3003
2496
|
func scriptedWireTag(v ${PKG}.Value) string {
|
|
@@ -3076,7 +2569,7 @@ func probeRowVal(v ${PKG}.Value) RowProbe {
|
|
|
3076
2569
|
return RowProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
|
|
3077
2570
|
}
|
|
3078
2571
|
if o, ok := v.(*${PKG}.Obj); ok {
|
|
3079
|
-
return RowProbe{Kind: probeGot, Got:
|
|
2572
|
+
return RowProbe{Kind: probeGot, Got: WireRow{obj: o}, ActualWireType: "M"}
|
|
3080
2573
|
}
|
|
3081
2574
|
return RowProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
|
|
3082
2575
|
}
|
|
@@ -3086,79 +2579,89 @@ func probeListVal(v ${PKG}.Value) ListProbe {
|
|
|
3086
2579
|
return ListProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
|
|
3087
2580
|
}
|
|
3088
2581
|
if a, ok := v.([]${PKG}.Value); ok {
|
|
3089
|
-
return ListProbe{Kind: probeGot, Got:
|
|
2582
|
+
return ListProbe{Kind: probeGot, Got: WireList{items: a}, ActualWireType: "L"}
|
|
3090
2583
|
}
|
|
3091
2584
|
return ListProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
|
|
3092
2585
|
}
|
|
3093
2586
|
|
|
3094
|
-
type
|
|
3095
|
-
|
|
2587
|
+
// WireValue — the CONCRETE scripted node-result wire (the in-package default seam type the covered runner
|
|
2588
|
+
// classifies via As*). A top value is always present, so As* never observes Absent; a scripted null is a
|
|
2589
|
+
// nil Value → Null. A real consumer supplies its own concrete WireValue over its AttributeValue payload.
|
|
2590
|
+
type WireValue struct {
|
|
2591
|
+
v ${PKG}.Value
|
|
3096
2592
|
}
|
|
3097
2593
|
|
|
3098
|
-
func
|
|
3099
|
-
|
|
3100
|
-
|
|
2594
|
+
func (w WireValue) AsString() StringProbe { return probeStringVal(w.v) }
|
|
2595
|
+
func (w WireValue) AsNumber() NumberProbe { return probeNumberVal(w.v) }
|
|
2596
|
+
func (w WireValue) AsBool() BoolProbe { return probeBoolVal(w.v) }
|
|
2597
|
+
func (w WireValue) AsRow() RowProbe { return probeRowVal(w.v) }
|
|
2598
|
+
func (w WireValue) AsList() ListProbe { return probeListVal(w.v) }
|
|
2599
|
+
|
|
2600
|
+
// WireRow / WireList — the CONCRETE scripted wire map / array (the in-package default seam types), probed
|
|
2601
|
+
// per declared field / element by the generated inline de-box (their methods return bc's probe structs).
|
|
2602
|
+
type WireRow struct {
|
|
2603
|
+
obj *${PKG}.Obj
|
|
3101
2604
|
}
|
|
3102
2605
|
|
|
3103
|
-
func (w
|
|
2606
|
+
func (w WireRow) field(name string) (${PKG}.Value, bool) {
|
|
3104
2607
|
if w.obj == nil {
|
|
3105
2608
|
return nil, false
|
|
3106
2609
|
}
|
|
3107
2610
|
return w.obj.Get(name)
|
|
3108
2611
|
}
|
|
3109
2612
|
|
|
3110
|
-
func (w
|
|
2613
|
+
func (w WireRow) Keys() []string {
|
|
3111
2614
|
if w.obj == nil {
|
|
3112
2615
|
return nil
|
|
3113
2616
|
}
|
|
3114
2617
|
return w.obj.Keys
|
|
3115
2618
|
}
|
|
3116
2619
|
|
|
3117
|
-
func (w
|
|
2620
|
+
func (w WireRow) ProbeString(field string) StringProbe {
|
|
3118
2621
|
if v, ok := w.field(field); ok {
|
|
3119
2622
|
return probeStringVal(v)
|
|
3120
2623
|
}
|
|
3121
2624
|
return StringProbe{Kind: probeAbsent}
|
|
3122
2625
|
}
|
|
3123
2626
|
|
|
3124
|
-
func (w
|
|
2627
|
+
func (w WireRow) ProbeNumber(field string) NumberProbe {
|
|
3125
2628
|
if v, ok := w.field(field); ok {
|
|
3126
2629
|
return probeNumberVal(v)
|
|
3127
2630
|
}
|
|
3128
2631
|
return NumberProbe{Kind: probeAbsent}
|
|
3129
2632
|
}
|
|
3130
2633
|
|
|
3131
|
-
func (w
|
|
2634
|
+
func (w WireRow) ProbeBool(field string) BoolProbe {
|
|
3132
2635
|
if v, ok := w.field(field); ok {
|
|
3133
2636
|
return probeBoolVal(v)
|
|
3134
2637
|
}
|
|
3135
2638
|
return BoolProbe{Kind: probeAbsent}
|
|
3136
2639
|
}
|
|
3137
2640
|
|
|
3138
|
-
func (w
|
|
2641
|
+
func (w WireRow) ProbeRow(field string) RowProbe {
|
|
3139
2642
|
if v, ok := w.field(field); ok {
|
|
3140
2643
|
return probeRowVal(v)
|
|
3141
2644
|
}
|
|
3142
2645
|
return RowProbe{Kind: probeAbsent}
|
|
3143
2646
|
}
|
|
3144
2647
|
|
|
3145
|
-
func (w
|
|
2648
|
+
func (w WireRow) ProbeList(field string) ListProbe {
|
|
3146
2649
|
if v, ok := w.field(field); ok {
|
|
3147
2650
|
return probeListVal(v)
|
|
3148
2651
|
}
|
|
3149
2652
|
return ListProbe{Kind: probeAbsent}
|
|
3150
2653
|
}
|
|
3151
2654
|
|
|
3152
|
-
type
|
|
2655
|
+
type WireList struct {
|
|
3153
2656
|
items []${PKG}.Value
|
|
3154
2657
|
}
|
|
3155
2658
|
|
|
3156
|
-
func (l
|
|
3157
|
-
func (l
|
|
3158
|
-
func (l
|
|
3159
|
-
func (l
|
|
3160
|
-
func (l
|
|
3161
|
-
func (l
|
|
2659
|
+
func (l WireList) Len() int { return len(l.items) }
|
|
2660
|
+
func (l WireList) ElemString(i int) StringProbe { return probeStringVal(l.items[i]) }
|
|
2661
|
+
func (l WireList) ElemNumber(i int) NumberProbe { return probeNumberVal(l.items[i]) }
|
|
2662
|
+
func (l WireList) ElemBool(i int) BoolProbe { return probeBoolVal(l.items[i]) }
|
|
2663
|
+
func (l WireList) ElemRow(i int) RowProbe { return probeRowVal(l.items[i]) }
|
|
2664
|
+
func (l WireList) ElemList(i int) ListProbe { return probeListVal(l.items[i]) }
|
|
3162
2665
|
|
|
3163
2666
|
// FailureDetail exposes the structured Error Value fields WITHOUT naming the local ErrorDetail type, so
|
|
3164
2667
|
// the harness can assert the de-box's kind / field / expectedType / actualWireType / rawValue (TEST
|
|
@@ -3171,21 +2674,17 @@ func (e *BehaviorError) FailureDetail() (kind, model, field, expectedType, actua
|
|
|
3171
2674
|
return d.Kind, d.Model, d.Field, d.ExpectedType, d.ActualWireType, d.RawValue, true
|
|
3172
2675
|
}`
|
|
3173
2676
|
: "";
|
|
3174
|
-
//
|
|
3175
|
-
//
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
//
|
|
3183
|
-
//
|
|
3184
|
-
// This is where the generic scripted source (a Value queue) meets the concrete per-node seam: the
|
|
3185
|
-
// decode is generated (it knows the concrete row type), so the runner stays fully concrete. ──
|
|
3186
|
-
const decodeFns = [];
|
|
2677
|
+
// Handler_<comp> wrapper types: the covered runner takes the CONCRETE per-component handler by value
|
|
2678
|
+
// (non-generic). One scripted queue backs every component, so each Handler_<comp> embeds the shared
|
|
2679
|
+
// *ScriptedNativeRaw — the Node_* methods (on *ScriptedNativeRaw) are promoted, so the runner's direct
|
|
2680
|
+
// h.Node_*(...) call resolves to a concrete method (no interface, no dictionary). Test glue only.
|
|
2681
|
+
const handlerWrappers = native
|
|
2682
|
+
.map((c) => `// ${handlerTypeName(c.name)} — the CONCRETE handler the '${c.name}' runner takes by value (embeds the\n// shared scripted queue; the Node_* methods are promoted).\ntype ${handlerTypeName(c.name)} struct {\n\t*ScriptedNativeRaw\n}`)
|
|
2683
|
+
.join("\n\n");
|
|
2684
|
+
// ── concrete scripted adapter (TEST glue): implements every Node_* method by draining a per-COMPONENT
|
|
2685
|
+
// scripted-outcome queue and returning the scripted ok Value wrapped as a WireValue (the generic
|
|
2686
|
+
// scripted source — a Value queue — meets the uniform CONCRETE wire seam). ──
|
|
3187
2687
|
const methodImpls = [];
|
|
3188
|
-
const emittedDecode = new Set();
|
|
3189
2688
|
for (const c of native) {
|
|
3190
2689
|
const typedNodes = new Map();
|
|
3191
2690
|
for (const n of c.body)
|
|
@@ -3196,16 +2695,12 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
|
3196
2695
|
continue; // #108: cond has no handler method (pure Expression) — no scripted impl.
|
|
3197
2696
|
const ref = typedNodes.get(n.id);
|
|
3198
2697
|
if ("fanout" in n) {
|
|
2698
|
+
// fanout: return the aligned per-id element WIRES ([]*WireValue); a null aligned body is a
|
|
2699
|
+
// DANGLING id → nil (the covered runner maps it to the zero element).
|
|
3199
2700
|
const f = n.fanout;
|
|
3200
|
-
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
3201
|
-
const elemT = rawElemStructName(c.name, n.id);
|
|
3202
|
-
const batchT = rawRowStructName(c.name, n.id);
|
|
3203
2701
|
const portsT = `${portsStructName(c.name, n.id)}_batch`;
|
|
3204
2702
|
const foBoundT = boundGoType(n, c, typedNodes, plan);
|
|
3205
|
-
|
|
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) {
|
|
2703
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${foBoundT}) ([]*WireValue, error) {
|
|
3209
2704
|
src, ok := s.next(${JSON.stringify(f.component)})
|
|
3210
2705
|
if !ok {
|
|
3211
2706
|
return nil, unknownComponent(${JSON.stringify(f.component)})
|
|
@@ -3214,54 +2709,27 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
|
3214
2709
|
return nil, leafFailure(src.err)
|
|
3215
2710
|
}
|
|
3216
2711
|
arr, _ := src.ok.([]${PKG}.Value)
|
|
3217
|
-
wires := make([]
|
|
2712
|
+
wires := make([]*WireValue, 0, len(arr))
|
|
3218
2713
|
for _, ev := range arr {
|
|
3219
2714
|
if ev == nil {
|
|
3220
2715
|
wires = append(wires, nil)
|
|
3221
2716
|
continue
|
|
3222
2717
|
}
|
|
3223
|
-
|
|
2718
|
+
wv := WireValue{v: ev}
|
|
2719
|
+
wires = append(wires, &wv)
|
|
3224
2720
|
}
|
|
3225
2721
|
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) {
|
|
3232
|
-
src, ok := s.next(${JSON.stringify(f.component)})
|
|
3233
|
-
if !ok {
|
|
3234
|
-
return ${batchT}{}, unknownComponent(${JSON.stringify(f.component)})
|
|
3235
|
-
}
|
|
3236
|
-
if src.isErr {
|
|
3237
|
-
return ${batchT}{}, leafFailure(src.err)
|
|
3238
|
-
}
|
|
3239
|
-
arr, _ := src.ok.([]${PKG}.Value)
|
|
3240
|
-
rows := make([]${elemT}, 0, len(arr))
|
|
3241
|
-
for _, ev := range arr {
|
|
3242
|
-
if ev == nil {
|
|
3243
|
-
rows = append(rows, ${elemT}{})
|
|
3244
|
-
continue
|
|
3245
|
-
}
|
|
3246
|
-
rows = append(rows, ${elemDecode}(ev))
|
|
3247
|
-
}
|
|
3248
|
-
return ${batchT}{Rows: rows}, nil
|
|
3249
2722
|
}`);
|
|
3250
2723
|
continue;
|
|
3251
2724
|
}
|
|
3252
2725
|
if ("map" in n) {
|
|
2726
|
+
// map: batched → one WIRE per element ([]WireValue); non-batched → one element WIRE (WireValue).
|
|
3253
2727
|
const m = n.map;
|
|
3254
2728
|
const batched = m.batched === true;
|
|
3255
|
-
const arrRef = ref;
|
|
3256
|
-
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
3257
|
-
const elemT = rawElemStructName(c.name, n.id);
|
|
3258
|
-
const batchT = rawRowStructName(c.name, n.id);
|
|
3259
2729
|
const portsT = batched ? `${portsStructName(c.name, n.id)}_batch` : portsStructName(c.name, n.id);
|
|
3260
2730
|
const mapBoundT = boundGoType(n, c, typedNodes, plan);
|
|
3261
|
-
if (
|
|
3262
|
-
|
|
3263
|
-
if (batched) {
|
|
3264
|
-
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) ([]WireRow, error) {
|
|
2731
|
+
if (batched) {
|
|
2732
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) ([]WireValue, error) {
|
|
3265
2733
|
src, ok := s.next(${JSON.stringify(m.component)})
|
|
3266
2734
|
if !ok {
|
|
3267
2735
|
return nil, unknownComponent(${JSON.stringify(m.component)})
|
|
@@ -3270,155 +2738,74 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
|
3270
2738
|
return nil, leafFailure(src.err)
|
|
3271
2739
|
}
|
|
3272
2740
|
arr, _ := src.ok.([]${PKG}.Value)
|
|
3273
|
-
wires := make([]
|
|
2741
|
+
wires := make([]WireValue, 0, len(arr))
|
|
3274
2742
|
for _, ev := range arr {
|
|
3275
|
-
wires = append(wires,
|
|
2743
|
+
wires = append(wires, WireValue{v: ev})
|
|
3276
2744
|
}
|
|
3277
2745
|
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");
|
|
3296
|
-
if (batched) {
|
|
3297
|
-
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${batchT}, error) {
|
|
3298
|
-
src, ok := s.next(${JSON.stringify(m.component)})
|
|
3299
|
-
if !ok {
|
|
3300
|
-
return ${batchT}{}, unknownComponent(${JSON.stringify(m.component)})
|
|
3301
|
-
}
|
|
3302
|
-
if src.isErr {
|
|
3303
|
-
return ${batchT}{}, leafFailure(src.err)
|
|
3304
|
-
}
|
|
3305
|
-
arr, _ := src.ok.([]${PKG}.Value)
|
|
3306
|
-
rows := make([]${elemT}, 0, len(arr))
|
|
3307
|
-
for _, ev := range arr {
|
|
3308
|
-
rows = append(rows, ${elemDecode}(ev))
|
|
3309
|
-
}
|
|
3310
|
-
return ${batchT}{Rows: rows}, nil
|
|
3311
2746
|
}`);
|
|
3312
2747
|
}
|
|
3313
2748
|
else {
|
|
3314
|
-
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (
|
|
2749
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (WireValue, error) {
|
|
3315
2750
|
src, ok := s.next(${JSON.stringify(m.component)})
|
|
3316
2751
|
if !ok {
|
|
3317
|
-
return
|
|
2752
|
+
return WireValue{}, unknownComponent(${JSON.stringify(m.component)})
|
|
3318
2753
|
}
|
|
3319
2754
|
if src.isErr {
|
|
3320
|
-
return
|
|
2755
|
+
return WireValue{}, leafFailure(src.err)
|
|
3321
2756
|
}
|
|
3322
|
-
return
|
|
2757
|
+
return WireValue{v: src.ok}, nil
|
|
3323
2758
|
}`);
|
|
3324
2759
|
}
|
|
3325
2760
|
continue;
|
|
3326
2761
|
}
|
|
2762
|
+
// componentRef: return the whole result WIRE (one WireValue wrapping the scripted ok Value); the
|
|
2763
|
+
// covered runner's INLINE de-box classifies it. A real consumer returns its own wire payload.
|
|
3327
2764
|
const node = n;
|
|
3328
|
-
const rowT = rawRowStructName(c.name, node.id);
|
|
3329
2765
|
const nodeBoundT = boundGoType(node, c, typedNodes, plan);
|
|
3330
|
-
//
|
|
3331
|
-
//
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
const
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
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
|
-
}
|
|
2766
|
+
// echo (#129/#141 fixtures): the scripted leaf can ECHO an input PORT back as its result wire, so the
|
|
2767
|
+
// input-port value flows into the de-boxed result and the runtime non-vacuity asserts survive.
|
|
2768
|
+
const echoBranches = [];
|
|
2769
|
+
if (ref.kind === "named") {
|
|
2770
|
+
const decl = findDecl(plan, ref.name);
|
|
2771
|
+
const field = decl !== undefined && decl.fields.length === 1 ? decl.fields[0] : undefined;
|
|
2772
|
+
const fieldGo = field !== undefined ? renderTypeRef(field.type) : null;
|
|
2773
|
+
const recordGo = renderTypeRef(ref);
|
|
2774
|
+
for (const pn of Object.keys(node.ports)) {
|
|
2775
|
+
let portGo = null;
|
|
2776
|
+
try {
|
|
2777
|
+
portGo = portFieldGoType(node.ports[pn], c, plan, typedNodes, `echo port '${pn}'`);
|
|
2778
|
+
}
|
|
2779
|
+
catch {
|
|
2780
|
+
portGo = null;
|
|
2781
|
+
}
|
|
2782
|
+
if (portGo === recordGo) {
|
|
2783
|
+
// the port IS the whole record → echo it DIRECTLY (matching run_behavior's plain echo:port).
|
|
2784
|
+
echoBranches.push(`\tif src.echo == ${JSON.stringify(pn)} {\n\t\treturn WireValue{v: ${serializeTyped(`ports.${goPortFieldName(pn)}`, ref, plan)}}, nil\n\t}`);
|
|
2785
|
+
}
|
|
2786
|
+
else if (fieldGo !== null && field !== undefined && portGo === fieldGo) {
|
|
2787
|
+
// a scalar port wraps into the record's single field ({v: ports.X}).
|
|
2788
|
+
echoBranches.push(`\tif src.echo == ${JSON.stringify(pn)} {\n\t\treturn WireValue{v: ${serializeTyped(`${recordGo}{${goFieldName(field.name)}: ports.${goPortFieldName(pn)}}`, ref, plan)}}, nil\n\t}`);
|
|
3367
2789
|
}
|
|
3368
2790
|
}
|
|
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) {
|
|
3390
|
-
src, ok := s.next(${JSON.stringify(node.component)})
|
|
3391
|
-
if !ok {
|
|
3392
|
-
return nil, unknownComponent(${JSON.stringify(node.component)})
|
|
3393
|
-
}
|
|
3394
|
-
if src.isErr {
|
|
3395
|
-
return nil, leafFailure(src.err)
|
|
3396
|
-
}
|
|
3397
|
-
${body}
|
|
3398
|
-
}`);
|
|
3399
|
-
continue;
|
|
3400
2791
|
}
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
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");
|
|
2792
|
+
else if (ref.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items")) {
|
|
2793
|
+
// #129: an arr node with an `items` port echoes the NATIVE port array (serialized to a Value array)
|
|
2794
|
+
// so the node→leaf array dataflow test is non-vacuous.
|
|
2795
|
+
echoBranches.push(`\tif src.echo == "items" {\n\t\treturn WireValue{v: ${serializeTyped(`ports.${goPortFieldName("items")}`, ref, plan)}}, nil\n\t}`);
|
|
2796
|
+
}
|
|
2797
|
+
const canEcho = echoBranches.length > 0;
|
|
3409
2798
|
const portsParam = canEcho ? "ports" : "_";
|
|
3410
|
-
const
|
|
3411
|
-
|
|
3412
|
-
: "";
|
|
3413
|
-
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, node.id)}(${portsParam} ${portsStructName(c.name, node.id)}, _ ${nodeBoundT}) (${rowT}, error) {
|
|
2799
|
+
const echoBlock = canEcho ? `${echoBranches.join("\n")}\n` : "";
|
|
2800
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, node.id)}(${portsParam} ${portsStructName(c.name, node.id)}, _ ${nodeBoundT}) (WireValue, error) {
|
|
3414
2801
|
src, ok := s.next(${JSON.stringify(node.component)})
|
|
3415
2802
|
if !ok {
|
|
3416
|
-
return
|
|
2803
|
+
return WireValue{}, unknownComponent(${JSON.stringify(node.component)})
|
|
3417
2804
|
}
|
|
3418
2805
|
if src.isErr {
|
|
3419
|
-
return
|
|
2806
|
+
return WireValue{}, leafFailure(src.err)
|
|
3420
2807
|
}
|
|
3421
|
-
${
|
|
2808
|
+
${echoBlock} return WireValue{v: src.ok}, nil
|
|
3422
2809
|
}`);
|
|
3423
2810
|
}
|
|
3424
2811
|
}
|
|
@@ -3433,10 +2820,10 @@ type scriptSrc struct {
|
|
|
3433
2820
|
ok ${PKG}.Value
|
|
3434
2821
|
}
|
|
3435
2822
|
|
|
3436
|
-
// ScriptedNativeRaw — the CONCRETE test handler:
|
|
3437
|
-
//
|
|
3438
|
-
//
|
|
3439
|
-
// EXPORTED so the runner glue (a separate package) can name it
|
|
2823
|
+
// ScriptedNativeRaw — the CONCRETE test handler: its Node_* methods drain a per-COMPONENT scripted queue
|
|
2824
|
+
// and return the ok Value wrapped as a WireValue (the covered runner de-boxes it inline). Each runner
|
|
2825
|
+
// takes a Handler_<comp> value that embeds this, so the Node_* calls stay fully concrete (no interface).
|
|
2826
|
+
// EXPORTED so the runner glue (a separate package) can name it.
|
|
3440
2827
|
type ScriptedNativeRaw struct {
|
|
3441
2828
|
mu sync.Mutex
|
|
3442
2829
|
queues map[string][]scriptSrc
|
|
@@ -3510,10 +2897,10 @@ func (s *ScriptedNativeRaw) next(component string) (scriptSrc, bool) {
|
|
|
3510
2897
|
}`;
|
|
3511
2898
|
return `// GENERATED test-only observe companion (bc#77) — NOT part of the covered module. Serializes the
|
|
3512
2899
|
// struct-native runner's result to a canonical Value for the equivalence pin, and carries the CONCRETE
|
|
3513
|
-
// scripted test handler (
|
|
3514
|
-
//
|
|
3515
|
-
// native +
|
|
3516
|
-
// byte-compare the STRUCT-only covered read against run_behavior. Same package as the
|
|
2900
|
+
// scripted test handler (ScriptedNativeRaw) + the CONCRETE WireValue/WireRow/WireList seam types +
|
|
2901
|
+
// per-component Handler_<comp> wrappers the runner takes. The consumer never uses this (it keeps the
|
|
2902
|
+
// model native + supplies its own concrete WireValue + Handler_<comp>); it exists only so the test
|
|
2903
|
+
// harness can byte-compare the STRUCT-only covered read against run_behavior. Same package as the module.
|
|
3517
2904
|
package behaviors
|
|
3518
2905
|
|
|
3519
2906
|
import (
|
|
@@ -3522,69 +2909,40 @@ ${anyDeBox ? '\t"strconv"\n\n' : ""} "sync"
|
|
|
3522
2909
|
${PKG} "${runtimeImport ?? "github.com/foo-ogawa/behavior-contracts/go"}"
|
|
3523
2910
|
)
|
|
3524
2911
|
|
|
3525
|
-
// scripted wire adapter (TEST glue) —
|
|
2912
|
+
// scripted wire adapter (TEST glue) — the CONCRETE WireValue/WireRow/WireList the covered module references.
|
|
3526
2913
|
${wireAdapter}
|
|
3527
2914
|
|
|
3528
2915
|
// typed -> Value serializers (TEST glue only — the covered module has none).
|
|
3529
2916
|
${serializers}
|
|
3530
2917
|
|
|
3531
|
-
// Value -> typed struct marshallers (+ must* helpers) for the
|
|
2918
|
+
// Value -> typed struct marshallers (+ must* helpers) for the input decoders (TEST glue only).
|
|
3532
2919
|
${marshallers}
|
|
3533
2920
|
|
|
3534
2921
|
${mustHelpers}
|
|
3535
2922
|
|
|
3536
2923
|
${arrOptHelpers}
|
|
3537
2924
|
|
|
3538
|
-
// per-node concrete-row decoders (scripted Value -> RawRow_/RawElem_ struct).
|
|
3539
|
-
${decodeFns.join("\n\n")}
|
|
3540
|
-
|
|
3541
2925
|
// input decoders (generic *Obj -> concrete In_<comp>; TEST glue, one decode at the observe boundary).
|
|
3542
2926
|
${inDecoders}
|
|
3543
2927
|
|
|
3544
2928
|
${adapter}
|
|
3545
2929
|
|
|
3546
|
-
|
|
2930
|
+
// per-component CONCRETE handler wrappers (the runner takes one by value — embeds the shared queue).
|
|
2931
|
+
${handlerWrappers}
|
|
3547
2932
|
|
|
3548
|
-
//
|
|
2933
|
+
// ScriptedNativeRaw method impls (one per covered node — the concrete Node_* seam returning WireValue).
|
|
3549
2934
|
${methodImpls.join("\n\n")}
|
|
3550
2935
|
|
|
3551
|
-
// ObserveNativeRaw drives the struct-native runner and serializes to a canonical Value (test only).
|
|
3552
|
-
//
|
|
3553
|
-
//
|
|
3554
|
-
//
|
|
3555
|
-
func ObserveNativeRaw
|
|
2936
|
+
// ObserveNativeRaw drives the struct-native runner and serializes to a canonical Value (test only). It
|
|
2937
|
+
// takes the CONCRETE *ScriptedNativeRaw and wraps it per component in the runner's Handler_<comp> value —
|
|
2938
|
+
// each enclosed RunNativeRawStruct_* call is a NON-generic call over a concrete handler (direct,
|
|
2939
|
+
// devirtualized per-node method calls), never an interface or a generic dictionary.
|
|
2940
|
+
func ObserveNativeRaw(name string, handlers *ScriptedNativeRaw, input *${PKG}.Obj) (${PKG}.Value, error) {
|
|
3556
2941
|
${arms}
|
|
3557
2942
|
return nil, ${PKG}.NewBehaviorFailure("UNKNOWN_ENTRY", "component '"+name+"' is not a covered read (fail-closed)")
|
|
3558
2943
|
}
|
|
3559
2944
|
`;
|
|
3560
2945
|
}
|
|
3561
|
-
/** emitDecodeRow — emit (once, deduped) a decoder `decodeRow_<comp>_<node><suffix>(v Value) <rowT>`
|
|
3562
|
-
* that materializes the scripted ok Value into the concrete row struct (typed fields = the outType /
|
|
3563
|
-
* elem-row type). Named: reuse the Value->struct marshaller + copy fields; scalar/arr/opt: materialize
|
|
3564
|
-
* into .Val. TEST glue only (the production consumer decodes its own wire payload). */
|
|
3565
|
-
function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix) {
|
|
3566
|
-
const fn = `decodeRow_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}${suffix}`;
|
|
3567
|
-
if (emitted.has(fn))
|
|
3568
|
-
return fn;
|
|
3569
|
-
emitted.add(fn);
|
|
3570
|
-
const lines = [];
|
|
3571
|
-
lines.push(`func ${fn}(v ${PKG}.Value) ${rowT} {`);
|
|
3572
|
-
lines.push(`\tvar out ${rowT}`);
|
|
3573
|
-
if (ref.kind === "named") {
|
|
3574
|
-
const decl = findDecl(plan, ref.name);
|
|
3575
|
-
lines.push(`\tt := ${goTypedInternals.materializeExpr("v", ref, plan)}`);
|
|
3576
|
-
for (const f of decl.fields) {
|
|
3577
|
-
lines.push(`\tout.${goFieldName(f.name)} = t.${goFieldName(f.name)}`);
|
|
3578
|
-
}
|
|
3579
|
-
}
|
|
3580
|
-
else {
|
|
3581
|
-
lines.push(`\tout.Val = ${goTypedInternals.materializeExpr("v", ref, plan)}`);
|
|
3582
|
-
}
|
|
3583
|
-
lines.push(`\treturn out`);
|
|
3584
|
-
lines.push(`}`);
|
|
3585
|
-
out.push(lines.join("\n"));
|
|
3586
|
-
return fn;
|
|
3587
|
-
}
|
|
3588
2946
|
/**
|
|
3589
2947
|
* goTypedNativeEmitter — language id `go-typed-native` (bc#77). Emits the generic straight-line
|
|
3590
2948
|
* body (for non-covered components / helpers) plus the COMBINED READ layer (native ports struct
|