behavior-contracts 0.8.5 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring.d.ts.map +1 -1
- package/dist/authoring.js +25 -1
- package/dist/authoring.js.map +1 -1
- package/dist/behavior.d.ts +23 -3
- package/dist/behavior.d.ts.map +1 -1
- package/dist/behavior.js +209 -11
- package/dist/behavior.js.map +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +12 -1
- package/dist/builder.js.map +1 -1
- package/dist/expr-eval.d.ts +7 -1
- package/dist/expr-eval.d.ts.map +1 -1
- package/dist/expr-eval.js +8 -1
- package/dist/expr-eval.js.map +1 -1
- package/dist/generator/core.d.ts +28 -0
- package/dist/generator/core.d.ts.map +1 -1
- package/dist/generator/core.js +1 -0
- package/dist/generator/core.js.map +1 -1
- package/dist/generator/emit-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 +923 -494
- package/dist/generator/emit-typed-native-go.js.map +1 -1
- package/dist/generator/emit-typed-native-rust.d.ts +9 -6
- package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-rust.js +950 -505
- package/dist/generator/emit-typed-native-rust.js.map +1 -1
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +12 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/plan.d.ts +43 -2
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +19 -5
- package/dist/plan.js.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
@@ -15,14 +28,34 @@ function goErr(code, msgExpr) {
|
|
|
15
28
|
return `&BehaviorError{Code: ${JSON.stringify(code)}, Message: ${msgExpr}}`;
|
|
16
29
|
}
|
|
17
30
|
/** emitBehaviorErrorType — the LOCAL concrete error type baked into the covered module so a runner can
|
|
18
|
-
* fail closed with ZERO bc-runtime import. `*BehaviorError` satisfies `error
|
|
31
|
+
* fail closed with ZERO bc-runtime import. `*BehaviorError` satisfies `error` and carries the leaf's
|
|
32
|
+
* structured Error Value (scp-error.md "The Error Value"). */
|
|
19
33
|
function emitBehaviorErrorType() {
|
|
20
|
-
return `//
|
|
34
|
+
return `// ErrorDetail — the structured, recoverable payload a failure carries (scp-error.md "The Error
|
|
35
|
+
// Value"). The LEAF produces it at the wire boundary (the only party holding both the declared type
|
|
36
|
+
// and the raw wire datum); the runner transports it verbatim. Concrete fields — no boxed Value, no
|
|
37
|
+
// serialized blob in a string: ExpectedType is Portable Type Notation, a rendering of a STATICALLY
|
|
38
|
+
// DECLARED type, so nothing walks a type at runtime. Kind is the closed set (typeMismatch /
|
|
39
|
+
// missingField / overflow), empty when the failure is not about a datum.
|
|
40
|
+
type ErrorDetail struct {
|
|
41
|
+
Kind string
|
|
42
|
+
Model string
|
|
43
|
+
Field string
|
|
44
|
+
ExpectedType string
|
|
45
|
+
ActualWireType string
|
|
46
|
+
RawValue string
|
|
47
|
+
Context map[string]string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
|
|
21
51
|
// covered runner returns *BehaviorError (which satisfies error) instead of a bc-runtime failure, so
|
|
22
52
|
// the fully-covered module imports ZERO bc runtime. Codes match run_behavior verbatim (byte-equal).
|
|
53
|
+
// Detail carries the leaf's structured Error Value across the seam so a caller can log / skip /
|
|
54
|
+
// repair / migrate rather than parse a message (nil when the failure is not about a datum).
|
|
23
55
|
type BehaviorError struct {
|
|
24
56
|
Code string
|
|
25
57
|
Message string
|
|
58
|
+
Detail *ErrorDetail
|
|
26
59
|
}
|
|
27
60
|
|
|
28
61
|
func (e *BehaviorError) Error() string { return e.Code + ": " + e.Message }
|
|
@@ -30,7 +63,48 @@ func (e *BehaviorError) Error() string { return e.Code + ": " + e.Message }
|
|
|
30
63
|
// FailureCode exposes the stable failure code (byte-equal to run_behavior) WITHOUT a bc-runtime type:
|
|
31
64
|
// a caller that wants the code type-asserts the small local interface { FailureCode() string } rather
|
|
32
65
|
// than a bc failure type — keeping this module runtime-free while a harness can still read the code.
|
|
33
|
-
func (e *BehaviorError) FailureCode() string { return e.Code }
|
|
66
|
+
func (e *BehaviorError) FailureCode() string { return e.Code }
|
|
67
|
+
|
|
68
|
+
// unknownComponent — the fail-closed failure for a catalog name with no handler (byte-equal to
|
|
69
|
+
// run_behavior). In the STATIC codegen model a missing impl is normally a compile error; this covers
|
|
70
|
+
// the scripted test glue's dynamic queue miss.
|
|
71
|
+
func unknownComponent(component string) *BehaviorError {
|
|
72
|
+
return &BehaviorError{Code: "UNKNOWN_COMPONENT", Message: "component '" + component + "' has no handler (fail-closed)"}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// leafFailure — a failure the LEAF reports (test glue): the runner assigns the node's failure code
|
|
76
|
+
// from its Error Policy Kind, so what a leaf carries is its message and its structured Error Value.
|
|
77
|
+
func leafFailure(message string) *BehaviorError {
|
|
78
|
+
return &BehaviorError{Code: "LEAF_FAILURE", Message: message}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// opFailed wraps a leaf failure as the node's Failure under its Error Policy Kind, carrying the
|
|
82
|
+
// leaf's structured detail through unchanged (the runner transports the Error Value, it does not
|
|
83
|
+
// synthesize or re-interpret it).
|
|
84
|
+
func opFailed(node string, policy string, e error) *BehaviorError {
|
|
85
|
+
if be, ok := e.(*BehaviorError); ok {
|
|
86
|
+
return &BehaviorError{Code: "OP_FAILED", Message: "operation '" + node + "' failed under '" + policy + "' policy: " + be.Message, Detail: be.Detail}
|
|
87
|
+
}
|
|
88
|
+
return &BehaviorError{Code: "OP_FAILED", Message: "operation '" + node + "' failed under '" + policy + "' policy: " + e.Error()}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// deTypeMismatch / deMissingField / deOverflow — the de-box mismatch failures the generated decode
|
|
92
|
+
// produces (scp-error.md "The Error Value"). Codes are byte-equal to the runtime outType check
|
|
93
|
+
// (TYPE_MISMATCH / MISSING_PROP) so the covered read stays equivalent to run_behavior; detail.kind
|
|
94
|
+
// distinguishes typeMismatch / missingField / overflow. ErrorDetail is built by a POSITIONAL literal so
|
|
95
|
+
// the offending value is carried without naming the field (the covered module stays whole-file free of
|
|
96
|
+
// the boxed result-type token; its only permitted occurrence is the ErrorDetail field declaration).
|
|
97
|
+
func deTypeMismatch(model, field, expected, actualWire, raw string) *BehaviorError {
|
|
98
|
+
return &BehaviorError{Code: "TYPE_MISMATCH", Message: "node '" + model + "': " + field + ": expected " + expected + ", got " + actualWire, Detail: &ErrorDetail{"typeMismatch", model, field, expected, actualWire, raw, nil}}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func deMissingField(model, field, expected string) *BehaviorError {
|
|
102
|
+
return &BehaviorError{Code: "MISSING_PROP", Message: "node '" + model + "': " + field + ": required field is absent", Detail: &ErrorDetail{"missingField", model, field, expected, "", "", nil}}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func deOverflow(model, field, expected, actualWire, raw string) *BehaviorError {
|
|
106
|
+
return &BehaviorError{Code: "TYPE_MISMATCH", Message: "node '" + model + "': " + field + ": " + raw + " is outside the range of " + expected, Detail: &ErrorDetail{"overflow", model, field, expected, actualWire, raw, nil}}
|
|
107
|
+
}`;
|
|
34
108
|
}
|
|
35
109
|
const { sanitize, emitExpr } = goStraightlineInternals;
|
|
36
110
|
const { goFieldName, renderTypeRef, goScalar, serializeTyped, emitTypeDecls, emitSerializers, emitTypedValue, emitMustHelpers, } = goTypedInternals;
|
|
@@ -39,6 +113,241 @@ const { goFieldName, renderTypeRef, goScalar, serializeTyped, emitTypeDecls, emi
|
|
|
39
113
|
function typedLocal(nodeId) {
|
|
40
114
|
return `t_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
41
115
|
}
|
|
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;
|
|
142
|
+
case "map":
|
|
143
|
+
base = `map[string]${goTypeOfPlan(plan.value)}`;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
return "*".repeat(plan.optDepth) + base;
|
|
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})`;
|
|
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);
|
|
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");
|
|
271
|
+
}
|
|
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) {
|
|
294
|
+
return `// Probe kinds — the outcome of classifying one wire attribute against a declared field type.
|
|
295
|
+
const (
|
|
296
|
+
probeGot uint8 = 0 // present; the wire variant matches the declared type
|
|
297
|
+
probeWrong uint8 = 1 // present but a different wire variant (carries ActualWireType + Raw)
|
|
298
|
+
probeAbsent uint8 = 2 // no attribute for this field
|
|
299
|
+
probeNull uint8 = 3 // present as the producer's null variant (opt → None; required → mismatch)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
// StringProbe / NumberProbe / BoolProbe — a scalar probe result. Got holds the matched value (a
|
|
303
|
+
// NumberProbe's Got is the raw numeric text; the de-box parses + range-checks it). ActualWireType is the
|
|
304
|
+
// producer's own wire tag (a free string, e.g. a DynamoDB "S"/"N"/"BOOL"); Raw is the offending value
|
|
305
|
+
// stringified. Concrete structs — no boxed Value crosses the seam.
|
|
306
|
+
type StringProbe struct {
|
|
307
|
+
Kind uint8
|
|
308
|
+
Got string
|
|
309
|
+
ActualWireType string
|
|
310
|
+
Raw string
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
type NumberProbe struct {
|
|
314
|
+
Kind uint8
|
|
315
|
+
Got string
|
|
316
|
+
ActualWireType string
|
|
317
|
+
Raw string
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
type BoolProbe struct {
|
|
321
|
+
Kind uint8
|
|
322
|
+
Got bool
|
|
323
|
+
ActualWireType string
|
|
324
|
+
Raw string
|
|
325
|
+
}
|
|
326
|
+
|
|
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.
|
|
330
|
+
type RowProbe struct {
|
|
331
|
+
Kind uint8
|
|
332
|
+
Got ${wire.row}
|
|
333
|
+
ActualWireType string
|
|
334
|
+
Raw string
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
type ListProbe struct {
|
|
338
|
+
Kind uint8
|
|
339
|
+
Got ${wire.list}
|
|
340
|
+
ActualWireType string
|
|
341
|
+
Raw string
|
|
342
|
+
}`;
|
|
343
|
+
}
|
|
344
|
+
/** mapFanoutElemRef — the element ROW TypeRef a map/fanout node decodes (the per-element record). */
|
|
345
|
+
function mapFanoutElemRef(node, typedNodes, plan) {
|
|
346
|
+
const ref = typedNodes.get(node.id);
|
|
347
|
+
if ("fanout" in node)
|
|
348
|
+
return fanoutItemsElemRef(node, ref, plan);
|
|
349
|
+
return mapElemRowRef(node, ref, plan);
|
|
350
|
+
}
|
|
42
351
|
function goPortFieldName(wire) {
|
|
43
352
|
const safe = wire.replace(/[^A-Za-z0-9_]/g, "_");
|
|
44
353
|
const head = safe.charAt(0);
|
|
@@ -49,21 +358,14 @@ function portsStructName(compName, nodeId) {
|
|
|
49
358
|
// type to implement the per-component Handler_<comp> interface's concrete Node_* method signatures.
|
|
50
359
|
return `PortsNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
51
360
|
}
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* field: every port lowers to a concrete Go type, and a port that cannot lower FAILS CLOSED. The field
|
|
56
|
-
* type is the compiled expression's TypeRef, so it can never disagree with the initializer (emitPortInit),
|
|
57
|
-
* which is the SAME lowering read for its value. `where` names the emission site for the error.
|
|
58
|
-
*/
|
|
59
|
-
function portFieldGoType(node, comp, plan, typedNodes, where = "port", opts) {
|
|
60
|
-
const c = compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), where, opts);
|
|
61
|
-
return c.renderedType ?? renderTypeRef(c.ref);
|
|
361
|
+
/** scalar TypeRef → PortFieldType (native go scalar kind). */
|
|
362
|
+
function goScalarPortField(scalar) {
|
|
363
|
+
return scalar === "string" ? "string" : scalar === "int" ? "int64" : scalar === "float" ? "float64" : "bool";
|
|
62
364
|
}
|
|
63
365
|
/**
|
|
64
366
|
* goNativeResolveHead — the ONE ref-head resolver the runtime-free native-expr compiler reads, for cond
|
|
65
|
-
* `if`/branches, a map/fanout `when` guard, AND a port
|
|
66
|
-
*
|
|
367
|
+
* `if`/branches, a map/fanout `when` guard, AND a port. A head is a `$as` element binding, a prior body
|
|
368
|
+
* node's typed local, or an input port.
|
|
67
369
|
*/
|
|
68
370
|
function goNativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
|
|
69
371
|
return (head) => {
|
|
@@ -78,18 +380,14 @@ function goNativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
|
|
|
78
380
|
/**
|
|
79
381
|
* compilePortNode — lower a port expression to its native ports-struct field via the runtime-free
|
|
80
382
|
* native-expr compiler (native-expr.ts): the ONE port lowering. The port's field TYPE is the compiled
|
|
81
|
-
* `ref`, its initializer the compiled `expr` —
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* returned here as-is; only the value site (emitPortInit) rejects it, since a ports-struct literal cannot
|
|
86
|
-
* host statements — the coverage gate/type site must still see it as covered.
|
|
383
|
+
* `ref`, its initializer the compiled `expr` — derived from a SINGLE lowering, so they cannot disagree.
|
|
384
|
+
* Any closed operator, a literal, a ref (input scalar/array/map, prior-node scalar/array, `$as` element
|
|
385
|
+
* field), a concat, a static array, an optional-input read — all resolve here. A port that cannot lower
|
|
386
|
+
* FAILS CLOSED. A FALLIBLE lowering (hoists statements) is returned as-is; only emitPortInit rejects it.
|
|
87
387
|
*/
|
|
88
388
|
function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
|
|
89
389
|
const resolveHead = goNativeResolveHead(comp, plan, typedNodes, isPrior, opts);
|
|
90
390
|
try {
|
|
91
|
-
// zeroOut is unreachable: a fallible port is rejected at emitPortInit (a ports-struct literal hosts no
|
|
92
|
-
// statements), so the backend never renders an early-return with it.
|
|
93
391
|
return new NativeExprCompiler(makeGoExprBackend("", resolveHead, plan)).compilePort(node);
|
|
94
392
|
}
|
|
95
393
|
catch (e) {
|
|
@@ -98,8 +396,73 @@ function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
|
|
|
98
396
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} ${JSON.stringify(node)} does not lower to a native Go value: ${e.message}`);
|
|
99
397
|
}
|
|
100
398
|
}
|
|
101
|
-
/**
|
|
102
|
-
*
|
|
399
|
+
/**
|
|
400
|
+
* portFieldGoType — the NATIVE Go type for a covered port field (bc#90 / runtime-free). The covered
|
|
401
|
+
* ports struct carries NO `dslcontracts.Value` field: every port lowers to a concrete Go type.
|
|
402
|
+
* - a static string-array (projection) → `[]string` (change #2);
|
|
403
|
+
* - a bare int/float number literal (e.g. Query `limit: 20`) → `int64`/`float64`;
|
|
404
|
+
* - a string/bool/scalar-input port → the scalar Go type (inferPortType).
|
|
405
|
+
* A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
|
|
406
|
+
* Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no
|
|
407
|
+
* `dslcontracts.Value` fallback on the native module). `where` names the emission site for the error.
|
|
408
|
+
*/
|
|
409
|
+
function portFieldGoType(node, comp, plan, typedNodes, where = "port", opts) {
|
|
410
|
+
const c = compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), where, opts);
|
|
411
|
+
return c.renderedType ?? renderTypeRef(c.ref);
|
|
412
|
+
}
|
|
413
|
+
function staticArrElems(node) {
|
|
414
|
+
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
415
|
+
return null;
|
|
416
|
+
const keys = Object.keys(node);
|
|
417
|
+
if (keys.length !== 1 || keys[0] !== "arr")
|
|
418
|
+
return null;
|
|
419
|
+
const arg = node.arr;
|
|
420
|
+
return Array.isArray(arg) ? arg : null;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* isNativeRawComponent — a component covered by the combined read emitter. Strictly sequential
|
|
424
|
+
* (no real concurrency), body is ALL componentRef (no map/cond), default 'fail'/'continue'
|
|
425
|
+
* policy, relationKind is single/connection or absent, and EVERY node carries outType + the
|
|
426
|
+
* component carries outputType. relationKind:single / bindField ARE the point — inline exec makes
|
|
427
|
+
* the child-present decision from the real parent result. Ports AND the output are evaluated via
|
|
428
|
+
* the primitive SSoT over a Value-space portScope (so a dynamic operator in a port or an
|
|
429
|
+
* obj-assembly output is fine — the SSoT handles fail-closed semantics; only the RESULT plane +
|
|
430
|
+
* PORT container go native). A covered SHAPE that lacks outType is a fail-closed error raised at
|
|
431
|
+
* emit (see assertTypedCoverage) — never a silent re-box.
|
|
432
|
+
*/
|
|
433
|
+
/**
|
|
434
|
+
* numLiteralGoExpr — a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
|
|
435
|
+
* `classifyExpr` deliberately leaves a bare number `dynamic` (its checked int/float range rules are
|
|
436
|
+
* the evaluate SSoT, expr.go evalNumberLiteral), so a plain number never reaches the str/bool/null
|
|
437
|
+
* static branch. But a number literal IS statically resolvable to a native Go Value: the SAME
|
|
438
|
+
* range check the runtime applies is performed HERE at generation time (integral literal in the
|
|
439
|
+
* safe 2^53-1 window → int64; a fractional/exponent literal → the checked float64). Returns the Go
|
|
440
|
+
* expression for the literal's Value, or null when the node is not a bare number OR the integral
|
|
441
|
+
* literal is out of the safe window (that out-of-range case keeps the runtime INVALID_LITERAL
|
|
442
|
+
* semantics — the port is NOT covered natively, so the whole component falls through, never a
|
|
443
|
+
* silently-wrong inline). Mirrors coderuntime.evalNumberLiteral exactly.
|
|
444
|
+
*/
|
|
445
|
+
const NUM_SAFE_INT = 9007199254740991; // 2^53 - 1 (expr.go safeInt)
|
|
446
|
+
function numLiteralGoExpr(node) {
|
|
447
|
+
if (typeof node !== "number" || !Number.isFinite(node))
|
|
448
|
+
return null;
|
|
449
|
+
if (Number.isInteger(node)) {
|
|
450
|
+
if (node < -NUM_SAFE_INT || node > NUM_SAFE_INT)
|
|
451
|
+
return null; // out of safe window → not covered (runtime INVALID_LITERAL)
|
|
452
|
+
return `${PKG}.Value(int64(${node}))`;
|
|
453
|
+
}
|
|
454
|
+
// fractional / exponent literal → checked float64 (finite, already guaranteed above).
|
|
455
|
+
return `${PKG}.Value(float64(${node}))`;
|
|
456
|
+
}
|
|
457
|
+
/** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
|
|
458
|
+
* resolvable to a native Go Value with no NewObj / no evaluate interpreter. */
|
|
459
|
+
/** planForComp — a TypePlan for a single component (its named types are component-scoped, so a
|
|
460
|
+
* one-component IR yields the same plan the full-IR build would for this component). */
|
|
461
|
+
function planForComp(comp) {
|
|
462
|
+
return buildTypePlan({ irVersion: 1, exprVersion: 2, components: [comp] });
|
|
463
|
+
}
|
|
464
|
+
/** buildTypedNodes — the prior-node typed-local TypeRef map for a component (control-gate nodes are
|
|
465
|
+
* typeless; a map node carries its produced-array ref). An untyped node is omitted (not an error here). */
|
|
103
466
|
function buildTypedNodes(comp, plan) {
|
|
104
467
|
const typedNodes = new Map();
|
|
105
468
|
for (const n of comp.body) {
|
|
@@ -114,25 +477,26 @@ function buildTypedNodes(comp, plan) {
|
|
|
114
477
|
}
|
|
115
478
|
return typedNodes;
|
|
116
479
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
480
|
+
/** mapAsBinding — the `$as` element binding for a map/fanout node's element ports (the over-element
|
|
481
|
+
* TypeRef), or undefined when the over source does not resolve (an uncovered map). */
|
|
482
|
+
function mapAsBinding(comp, node, typedNodes, plan) {
|
|
483
|
+
try {
|
|
484
|
+
return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
125
489
|
}
|
|
126
|
-
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
490
|
+
/** portIsStatic — the ELIGIBILITY gate ASKS the one port lowering (compilePortNode / native-expr)
|
|
491
|
+
* whether it can lower this port, rather than re-deriving the shape rules — so the gate and the emitter
|
|
492
|
+
* can never disagree. A port that does not lower (or lowers fallibly) fails closed. */
|
|
129
493
|
function portIsStatic(node, comp, plan, typedNodes, asBinding) {
|
|
130
494
|
try {
|
|
131
495
|
compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), "port", asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined);
|
|
132
496
|
return true;
|
|
133
497
|
}
|
|
134
498
|
catch {
|
|
135
|
-
return false;
|
|
499
|
+
return false;
|
|
136
500
|
}
|
|
137
501
|
}
|
|
138
502
|
/** The output must lower to a typed struct/value with no Value-scope read: a ref whose head is a
|
|
@@ -180,8 +544,8 @@ function isFullyTyped(comp) {
|
|
|
180
544
|
}
|
|
181
545
|
return true;
|
|
182
546
|
}
|
|
183
|
-
function isNativeRaw(comp
|
|
184
|
-
return isSequentialComponentRefRead(comp
|
|
547
|
+
function isNativeRaw(comp) {
|
|
548
|
+
return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
|
|
185
549
|
}
|
|
186
550
|
/**
|
|
187
551
|
* coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet). A BATCHED `map` node whose:
|
|
@@ -196,7 +560,7 @@ function isNativeRaw(comp, plan) {
|
|
|
196
560
|
* element struct by copying the over element's typed fields + materializing the into field from the
|
|
197
561
|
* aligned batch raw — NO boxed Value on any plane.
|
|
198
562
|
*/
|
|
199
|
-
function coveredMapNode(n, bodyIds, comp
|
|
563
|
+
function coveredMapNode(n, bodyIds, comp) {
|
|
200
564
|
if (n === null || typeof n !== "object" || !("map" in n))
|
|
201
565
|
return false;
|
|
202
566
|
const m = n.map;
|
|
@@ -213,7 +577,7 @@ function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
|
|
|
213
577
|
// A map node must carry an outType (its ELEMENT type per the recorder SoT — authoring.ts recordMap
|
|
214
578
|
// holds `node.outType = mapElem`, the PRE-array-wrap element; the emitter synthesizes the produced
|
|
215
579
|
// `[]element`). A map WITHOUT outType is uncovered. Legacy hand-built IR that already carries the
|
|
216
|
-
//
|
|
580
|
+
// The map node's outType is the ELEMENT type; mapNodeArrRef wraps it to the produced array ref.
|
|
217
581
|
const outT = n.outType;
|
|
218
582
|
if (outT === undefined || typeof outT !== "object")
|
|
219
583
|
return false;
|
|
@@ -233,22 +597,14 @@ function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
|
|
|
233
597
|
return false;
|
|
234
598
|
}
|
|
235
599
|
const ports = m.ports;
|
|
600
|
+
const plan = planForComp(comp);
|
|
601
|
+
const typedNodes = buildTypedNodes(comp, plan);
|
|
236
602
|
const asBinding = mapAsBinding(comp, n, typedNodes, plan);
|
|
237
603
|
for (const p of Object.keys(ports))
|
|
238
604
|
if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
|
|
239
605
|
return false;
|
|
240
606
|
return true;
|
|
241
607
|
}
|
|
242
|
-
/** the `$as` element binding for a covered map/fanout node's element ports (name + over-element TypeRef),
|
|
243
|
-
* or undefined when the over element type does not resolve (the node is then uncovered upstream). */
|
|
244
|
-
function mapAsBinding(comp, node, typedNodes, plan) {
|
|
245
|
-
try {
|
|
246
|
-
return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
return undefined;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
608
|
/**
|
|
253
609
|
* coveredFanoutNode (v3) — a FANOUT node (first-class `@refs` id-list fan-out) is covered when:
|
|
254
610
|
* - over is a static ref to a PRIOR BODY NODE's typed arr field OR an INPUT array port with elemType,
|
|
@@ -257,7 +613,7 @@ function mapAsBinding(comp, node, typedNodes, plan) {
|
|
|
257
613
|
* The fanout de-boxes to: one deduped batched dispatch → first-seen dedupe (by the elem's dedupeKey
|
|
258
614
|
* field) + dangling drop + implicitSource strip + connection wrap {items, cursor:nil} — NO boxed Value.
|
|
259
615
|
*/
|
|
260
|
-
function coveredFanoutNode(n, bodyIds, comp
|
|
616
|
+
function coveredFanoutNode(n, bodyIds, comp) {
|
|
261
617
|
if (n === null || typeof n !== "object" || !("fanout" in n))
|
|
262
618
|
return false;
|
|
263
619
|
const f = n.fanout;
|
|
@@ -277,6 +633,8 @@ function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
|
|
|
277
633
|
return false;
|
|
278
634
|
}
|
|
279
635
|
const ports = f.ports;
|
|
636
|
+
const plan = planForComp(comp);
|
|
637
|
+
const typedNodes = buildTypedNodes(comp, plan);
|
|
280
638
|
let asBinding;
|
|
281
639
|
try {
|
|
282
640
|
asBinding = { name: f.as, ref: fanoutOverElemInfo(comp, n, typedNodes, plan).elemRef, plan };
|
|
@@ -294,8 +652,8 @@ function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
|
|
|
294
652
|
* reads. A real-concurrency plan whose parallel members include a map / bindField-relation child is
|
|
295
653
|
* NOT covered here (falls through) — the static-parallel orchestration covers the componentRef fan-out
|
|
296
654
|
* (the GroupAccessRead shape); a parallel map/relation-child stage is a further shape (out of scope). */
|
|
297
|
-
function isSequentialComponentRefRead(comp
|
|
298
|
-
return coverageRejectReason(comp
|
|
655
|
+
function isSequentialComponentRefRead(comp) {
|
|
656
|
+
return coverageRejectReason(comp) === null;
|
|
299
657
|
}
|
|
300
658
|
/**
|
|
301
659
|
* coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89). Returns
|
|
@@ -305,11 +663,12 @@ function isSequentialComponentRefRead(comp, plan) {
|
|
|
305
663
|
* never disagree. This is the message the fail-closed dispatch surfaces per non-native component — the
|
|
306
664
|
* user's signal to add native coverage, fix a genuinely-dynamic spec, or use the --ir dynamic surface.
|
|
307
665
|
*/
|
|
308
|
-
function coverageRejectReason(comp
|
|
666
|
+
function coverageRejectReason(comp) {
|
|
667
|
+
const crPlan = planForComp(comp);
|
|
668
|
+
const crTypedNodes = buildTypedNodes(comp, crPlan);
|
|
309
669
|
const ep = execPlan(comp);
|
|
310
670
|
if (ep === null)
|
|
311
671
|
return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
|
|
312
|
-
const typedNodes = buildTypedNodes(comp, plan);
|
|
313
672
|
// A parallel-stage member must be a componentRef node (no map) so the static parallel orchestration
|
|
314
673
|
// is a clean fan-out — the bounded-concurrency error-precedence protocol is proven for that shape.
|
|
315
674
|
// A map child inside a parallel stage still falls through (its per-element loop is a further shape).
|
|
@@ -354,7 +713,7 @@ function coverageRejectReason(comp, plan) {
|
|
|
354
713
|
if ("fanout" in n) {
|
|
355
714
|
// v3: a first-class fanout node (deduped connection list-in fan-out) IS covered when its over is a
|
|
356
715
|
// covered ref (prior-node arr / input array with elemType) and its element ports are static.
|
|
357
|
-
if (!coveredFanoutNode(n, bodyIds, comp
|
|
716
|
+
if (!coveredFanoutNode(n, bodyIds, comp))
|
|
358
717
|
return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
|
|
359
718
|
continue;
|
|
360
719
|
}
|
|
@@ -362,7 +721,7 @@ function coverageRejectReason(comp, plan) {
|
|
|
362
721
|
// #86 pt2 batched map...into / #93 shape 2/3 / #108 guarded map (no into) / map-over-input (with
|
|
363
722
|
// elemType) ARE covered. A non-covered map shape (guard+into heterogeneous / over-input without
|
|
364
723
|
// elemType) falls through to the loud fail-closed reject.
|
|
365
|
-
if (!coveredMapNode(n, bodyIds, comp
|
|
724
|
+
if (!coveredMapNode(n, bodyIds, comp))
|
|
366
725
|
return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
|
|
367
726
|
continue;
|
|
368
727
|
}
|
|
@@ -393,7 +752,7 @@ function coverageRejectReason(comp, plan) {
|
|
|
393
752
|
if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
394
753
|
return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
|
|
395
754
|
for (const p of Object.keys(cr.ports))
|
|
396
|
-
if (portIsStatic(cr.ports[p], comp,
|
|
755
|
+
if (portIsStatic(cr.ports[p], comp, crPlan, crTypedNodes) === false)
|
|
397
756
|
return `node '${n.id}' port '${p}' is not statically resolvable`;
|
|
398
757
|
}
|
|
399
758
|
if (!outputIsNativeLowerable(comp))
|
|
@@ -401,20 +760,25 @@ function coverageRejectReason(comp, plan) {
|
|
|
401
760
|
return null;
|
|
402
761
|
}
|
|
403
762
|
/** Fail closed if a covered read SHAPE lacks the typed annotations (no silent boxed re-box). */
|
|
404
|
-
function assertTypedCoverage(comp
|
|
405
|
-
if (!isSequentialComponentRefRead(comp
|
|
763
|
+
function assertTypedCoverage(comp) {
|
|
764
|
+
if (!isSequentialComponentRefRead(comp))
|
|
406
765
|
return;
|
|
407
766
|
if (isFullyTyped(comp))
|
|
408
767
|
return;
|
|
409
768
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#77): component '${comp.name}' is a sequential componentRef read but lacks outType/outputType on every node — the combined emitter de-boxes the RESULT into the node's outType struct, so a covered read MUST be fully typed (bc#45). Add outType annotations, or regenerate on go-straightline-native (boxed result).`);
|
|
410
769
|
}
|
|
411
770
|
// ── native ports struct + PortReader ──────────────────────────────────────────────────
|
|
412
|
-
function emitPortsStruct(comp, node, plan, typedNodes
|
|
771
|
+
function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
|
|
413
772
|
const structName = portsStructName(comp.name, node.id);
|
|
414
773
|
const portNames = Object.keys(node.ports);
|
|
415
774
|
if (portNames.length === 0) {
|
|
416
775
|
return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
|
|
417
776
|
}
|
|
777
|
+
// a node WITH ports always resolves through the one port lowering, which needs the type plan + the
|
|
778
|
+
// prior-node map (both are supplied by every caller — narrow the optional params for the fields below).
|
|
779
|
+
if (plan === undefined || typedNodes === undefined) {
|
|
780
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native: ports struct for node '${node.id}' needs a type plan + prior-node map`);
|
|
781
|
+
}
|
|
418
782
|
const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp, plan, typedNodes, `port '${p}' of node '${node.id}'`, asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined));
|
|
419
783
|
const fieldNames = portNames.map(goPortFieldName);
|
|
420
784
|
const fields = portNames
|
|
@@ -430,32 +794,11 @@ type ${structName} struct {
|
|
|
430
794
|
${fields}
|
|
431
795
|
}`;
|
|
432
796
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
// per-component `Handler_<comp>` interface has one method per node returning that concrete struct;
|
|
439
|
-
// the runner reads `row.<Field>` DIRECTLY (no `.(RawRow)`, no `.(scalar)`, no `RawValue`) and copies
|
|
440
|
-
// each field into the node's outType struct local. The `.(T)` type-assert seam that the generic
|
|
441
|
-
// RawValue de-box carried is GONE on the covered path (the handler produces the typed fields).
|
|
442
|
-
//
|
|
443
|
-
// A scalar/arr/opt node (outType not a named struct) has a single `Val <typed>` payload field. A
|
|
444
|
-
// named-struct node has one Go field per outType field (same names/types as the outType decl), so
|
|
445
|
-
// `RawRow_<comp>_<node>` is structurally the outType struct + {IsError, Err}. The copier is then a
|
|
446
|
-
// mechanical field-for-field assignment into the outType struct — NO dynamic value read anywhere.
|
|
447
|
-
/** Row_<comp>_<node> — the concrete per-node handler-result row struct name. (Deliberately NOT
|
|
448
|
-
* prefixed with the generic dynamic-accessor name — that accessor is what the concrete path removes.) */
|
|
449
|
-
function rawRowStructName(compName, nodeId) {
|
|
450
|
-
return `Row_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
451
|
-
}
|
|
452
|
-
/** RowElem_<comp>_<node> — the concrete per-ELEMENT row struct name for a covered map node (the
|
|
453
|
-
* element/into row the map handler yields per over element). */
|
|
454
|
-
function rawElemStructName(compName, nodeId) {
|
|
455
|
-
return `RowElem_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
456
|
-
}
|
|
457
|
-
/** Handler_<comp> — the per-component concrete handler interface name (one Node_* method per node). */
|
|
458
|
-
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) {
|
|
459
802
|
return `Handler_${sanitize(compName)}`;
|
|
460
803
|
}
|
|
461
804
|
/** RunNativeRawStruct_<comp> — the EXPORTED name of the per-component struct-returning combined runner
|
|
@@ -470,76 +813,6 @@ function runnerName(compName) {
|
|
|
470
813
|
function nodeMethodName(compName, nodeId) {
|
|
471
814
|
return `Node_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
472
815
|
}
|
|
473
|
-
/**
|
|
474
|
-
* emitRawRowStructs — emit the concrete `RawRow_<comp>_<node>` struct per covered node (and, for a
|
|
475
|
-
* covered map node, the per-element `RawElem_<comp>_<node>`). A named-struct outType flattens to one
|
|
476
|
-
* Go field per outType field; a scalar/arr/opt node carries a single `Val` payload. Every row carries
|
|
477
|
-
* the per-node error signal (IsError/Err) so the runner interprets policy without a boxed outcome.
|
|
478
|
-
*/
|
|
479
|
-
function emitRawRowStructs(comp, typedNodes, plan) {
|
|
480
|
-
const parts = [];
|
|
481
|
-
for (const n of comp.body) {
|
|
482
|
-
// #108: a cond node has NO handler-result row (pure Expression — no dispatch).
|
|
483
|
-
if ("cond" in n)
|
|
484
|
-
continue;
|
|
485
|
-
const ref = typedNodes.get(n.id);
|
|
486
|
-
if ("fanout" in n) {
|
|
487
|
-
// a covered fanout node: RawElem_ is the connection items element (the per-body row the batched
|
|
488
|
-
// handler yields, aligned to the deduped id list); the batch RawRow_ carries the aligned []RawElem_.
|
|
489
|
-
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
490
|
-
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
491
|
-
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
|
|
492
|
-
`type ${rawRowStructName(comp.name, n.id)} struct {\n\tIsError bool\n\tErr string\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
if ("map" in n) {
|
|
496
|
-
// a covered map node: RawElem_<comp>_<node> is the HANDLER'S per-element return shape:
|
|
497
|
-
// - into: the `into` field's type (the fetched item merged onto every over element);
|
|
498
|
-
// - no-into (#93 shape 3): the element outType directly (connection-collect).
|
|
499
|
-
// The batched Node_* returns RawRow_<comp>_<node> (aligned []RawElem_ + error); a per-element
|
|
500
|
-
// dispatch Node_* returns RawElem_ directly (one physical call per element).
|
|
501
|
-
const arrRef = ref;
|
|
502
|
-
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
503
|
-
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
504
|
-
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
|
|
505
|
-
`type ${rawRowStructName(comp.name, n.id)} struct {\n\tIsError bool\n\tErr string\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
|
-
parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
|
|
509
|
-
}
|
|
510
|
-
return parts.join("\n\n");
|
|
511
|
-
}
|
|
512
|
-
/** Emit one concrete row struct: flatten a named outType to typed fields, else a single Val payload;
|
|
513
|
-
* always carry IsError/Err. */
|
|
514
|
-
function emitOneRowStruct(name, ref, plan) {
|
|
515
|
-
const lines = [];
|
|
516
|
-
lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType; + error signal).`);
|
|
517
|
-
lines.push(`type ${name} struct {`);
|
|
518
|
-
lines.push(`\tIsError bool`);
|
|
519
|
-
lines.push(`\tErr string`);
|
|
520
|
-
if (ref.kind === "named") {
|
|
521
|
-
const decl = findDecl(plan, ref.name);
|
|
522
|
-
for (const f of decl.fields) {
|
|
523
|
-
lines.push(`\t${goFieldName(f.name)} ${renderTypeRef(f.type)} // ${JSON.stringify(f.name)}`);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
else {
|
|
527
|
-
lines.push(`\tVal ${renderTypeRef(ref)}`);
|
|
528
|
-
}
|
|
529
|
-
lines.push(`}`);
|
|
530
|
-
return lines.join("\n");
|
|
531
|
-
}
|
|
532
|
-
/** emitRowCopy — the statements copying a concrete row's typed fields into the node's outType local
|
|
533
|
-
* `target` (a value of Go type renderTypeRef(ref)). Named: field-for-field; scalar/arr/opt: `.Val`.
|
|
534
|
-
* `rowExpr` is the concrete RawRow_/RawElem_ value; `indent` tabs. No dynamic read — pure assignment. */
|
|
535
|
-
function emitRowCopy(target, rowExpr, ref, plan, indent) {
|
|
536
|
-
const t = "\t".repeat(indent);
|
|
537
|
-
if (ref.kind === "named") {
|
|
538
|
-
const decl = findDecl(plan, ref.name);
|
|
539
|
-
return decl.fields.map((f) => `${t}${target}.${goFieldName(f.name)} = ${rowExpr}.${goFieldName(f.name)}`).join("\n");
|
|
540
|
-
}
|
|
541
|
-
return `${t}${target} = ${rowExpr}.Val`;
|
|
542
|
-
}
|
|
543
816
|
/**
|
|
544
817
|
* boundGoType (change #3 — typed `bound`) — the CONCRETE Go type of a covered node's `bound` handler
|
|
545
818
|
* param (bc#90 / runtime-free). `bound` is the parent-produced key (the bindField value); it replaces
|
|
@@ -569,47 +842,6 @@ function boundGoType(node, comp, typedNodes, plan) {
|
|
|
569
842
|
return `*${renderTypeRef(bfRef.inner)}`;
|
|
570
843
|
return `*${renderTypeRef(bfRef)}`;
|
|
571
844
|
}
|
|
572
|
-
/**
|
|
573
|
-
* emitHandlerInterface — the per-component `Handler_<comp>` interface: one concrete-typed `Node_*`
|
|
574
|
-
* method per covered node. Each componentRef node method takes the node's native ports struct + the
|
|
575
|
-
* typed `bound` pointer (change #3 — the produced-aware parent key; NO boxed dslcontracts.Value) and
|
|
576
|
-
* returns its concrete row struct + a resolved bool. A covered map node's method takes the batch
|
|
577
|
-
* ports struct (batched) or the element ports struct (per-element) and returns the batch / element
|
|
578
|
-
* row. This is the fully-concrete, runtime-free replacement for the generic ExecNativeRaw seam.
|
|
579
|
-
*/
|
|
580
|
-
function emitHandlerInterface(comp, typedNodes, plan) {
|
|
581
|
-
const lines = [];
|
|
582
|
-
lines.push(`// ${handlerIfaceName(comp.name)} — the CONCRETE per-component handler seam: one typed method`);
|
|
583
|
-
lines.push(`// per covered node (native ports struct IN, concrete row struct OUT). No generic boxed-ports`);
|
|
584
|
-
lines.push(`// / boxed-value / dynamic accessor crosses the covered boundary — the consumer implements each`);
|
|
585
|
-
lines.push(`// Node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
|
|
586
|
-
lines.push(`type ${handlerIfaceName(comp.name)} interface {`);
|
|
587
|
-
for (const n of comp.body) {
|
|
588
|
-
if ("cond" in n)
|
|
589
|
-
continue; // #108: a cond node has no handler method (pure Expression).
|
|
590
|
-
const method = nodeMethodName(comp.name, n.id);
|
|
591
|
-
const bt = boundGoType(n, comp, typedNodes, plan);
|
|
592
|
-
if ("fanout" in n) {
|
|
593
|
-
// fanout (v3): one batched dispatch of the deduped id list. Ports = the batch struct; returns the
|
|
594
|
-
// aligned batch row (per-body rows) + resolved. The runner applies dedupe/drop/wrap after.
|
|
595
|
-
const portsT = `${portsStructName(comp.name, n.id)}_batch`;
|
|
596
|
-
const retT = rawRowStructName(comp.name, n.id);
|
|
597
|
-
lines.push(`\t${method}(ports ${portsT}, bound ${bt}) (${retT}, bool)`);
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
if ("map" in n) {
|
|
601
|
-
const m = n.map;
|
|
602
|
-
const batched = m.batched === true;
|
|
603
|
-
const portsT = batched ? `${portsStructName(comp.name, n.id)}_batch` : portsStructName(comp.name, n.id);
|
|
604
|
-
const retT = batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
|
|
605
|
-
lines.push(`\t${method}(ports ${portsT}, bound ${bt}) (${retT}, bool)`);
|
|
606
|
-
continue;
|
|
607
|
-
}
|
|
608
|
-
lines.push(`\t${method}(ports ${portsStructName(comp.name, n.id)}, bound ${bt}) (${rawRowStructName(comp.name, n.id)}, bool)`);
|
|
609
|
-
}
|
|
610
|
-
lines.push(`}`);
|
|
611
|
-
return lines.join("\n");
|
|
612
|
-
}
|
|
613
845
|
/** mapElemMaterializerName — the augmented-element copier for a covered map...into node. */
|
|
614
846
|
function mapElemMaterializerName(compName, nodeId) {
|
|
615
847
|
return `materializeMapElemNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
|
|
@@ -629,7 +861,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
|
|
|
629
861
|
// struct element). A pure-static/concat port ignores the binding.
|
|
630
862
|
const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
631
863
|
const asBinding = { name: m.as, ref: elemRef, plan };
|
|
632
|
-
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, plan, typedNodes
|
|
864
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan, typedNodes);
|
|
633
865
|
const batchStruct = `${structName}_batch`;
|
|
634
866
|
return `${elemStruct}
|
|
635
867
|
|
|
@@ -647,7 +879,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
|
|
|
647
879
|
const structName = portsStructName(comp.name, node.id);
|
|
648
880
|
const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
|
|
649
881
|
const asBinding = { name: f.as, ref: elemRef, plan };
|
|
650
|
-
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, plan, typedNodes
|
|
882
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan, typedNodes);
|
|
651
883
|
const batchStruct = `${structName}_batch`;
|
|
652
884
|
return `${elemStruct}
|
|
653
885
|
|
|
@@ -693,18 +925,16 @@ function fanoutOverElemInfo(comp, node, typedNodes, plan) {
|
|
|
693
925
|
}
|
|
694
926
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go fanout (v3): fanout '${node.id}' 'over' must be a ref to a prior node's typed arr field OR an input array port with a declared elemType (got ${JSON.stringify(f.over)}).`);
|
|
695
927
|
}
|
|
696
|
-
/** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
* Both converge to the SAME produced-array ref, so existing goldens stay byte-identical. */
|
|
928
|
+
/** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType`.
|
|
929
|
+
*
|
|
930
|
+
* A map node's `outType` is its ELEMENT type (scp-error.md "Map element type"), so the produced array
|
|
931
|
+
* is `{arr: element}`. This is the same reading the compiler applies when it resolves what a node
|
|
932
|
+
* result reference has (`type-gate`: `"map" in n ? {arr:ot} : ot`) — the emitter follows that SSoT
|
|
933
|
+
* rather than inspecting the annotation's shape, so an element that is ITSELF an `arr` stays
|
|
934
|
+
* expressible (element `arr(T)` produces `arr(arr(T))`). */
|
|
704
935
|
function mapNodeArrRef(node, plan) {
|
|
705
936
|
const outT = node.outType;
|
|
706
|
-
|
|
707
|
-
return deriveTypeRef((isArr ? outT : { arr: outT }), plan);
|
|
937
|
+
return deriveTypeRef({ arr: outT }, plan);
|
|
708
938
|
}
|
|
709
939
|
/** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
|
|
710
940
|
* type (into) or the element outType directly (no-into). This is the type of RawElem_<comp>_<node>. */
|
|
@@ -724,9 +954,8 @@ function mapElemRowRef(node, arrRef, plan) {
|
|
|
724
954
|
}
|
|
725
955
|
/**
|
|
726
956
|
* emitMapElemMaterializers — for each covered map...into node, emit the AUGMENTED-element copier: copy
|
|
727
|
-
* the over element's typed fields +
|
|
728
|
-
*
|
|
729
|
-
* 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).
|
|
730
959
|
*/
|
|
731
960
|
function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
732
961
|
const parts = [];
|
|
@@ -748,16 +977,15 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
|
748
977
|
const intoRowRef = mapElemRowRef(n, arrRef, plan);
|
|
749
978
|
const fn = mapElemMaterializerName(comp.name, n.id);
|
|
750
979
|
const overElemGo = mapOverElemType(comp, n, typedNodes, plan);
|
|
751
|
-
const elemRowGo = rawElemStructName(comp.name, n.id);
|
|
752
980
|
const lines = [];
|
|
753
981
|
lines.push(`// ${fn} — assemble the augmented map element '${n.id}': copy the over element's typed`);
|
|
754
|
-
lines.push(`// fields + set the '${intoKey}' (into) field from the
|
|
755
|
-
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} {`);
|
|
756
984
|
lines.push(`\tvar out ${elemRef.name}`);
|
|
757
985
|
for (const f of overFields) {
|
|
758
986
|
lines.push(`\tout.${goFieldName(f.name)} = over.${goFieldName(f.name)}`);
|
|
759
987
|
}
|
|
760
|
-
lines.push(
|
|
988
|
+
lines.push(`\tout.${goFieldName(intoKey)} = into`);
|
|
761
989
|
lines.push(`\treturn out`);
|
|
762
990
|
lines.push(`}`);
|
|
763
991
|
parts.push(lines.join("\n"));
|
|
@@ -847,31 +1075,17 @@ function reconstructG(e) {
|
|
|
847
1075
|
function inStructName(compName) {
|
|
848
1076
|
return `In_${sanitize(compName)}`;
|
|
849
1077
|
}
|
|
850
|
-
/**
|
|
851
|
-
* inputPortGoType — the native Go type for an input port's declared type. Scalars lower to the concrete
|
|
852
|
-
* scalar; `array`/`map` with a declared elemType to `[]ElemT` / `map[string]ElemT`; an OPTIONAL port
|
|
853
|
-
* (`{opt:T}` → `required:false`) to `*T` — the native representation of "the value is absent" (nil).
|
|
854
|
-
* A declared type with no native lowering stays the boxed Value.
|
|
855
|
-
*
|
|
856
|
-
* An OPTIONAL port whose inner type has no native lowering FAILS CLOSED: a boxed `Value` cannot carry
|
|
857
|
-
* the presence distinction on the covered plane, and emitting the bare inner type would silently turn
|
|
858
|
-
* "absent" into a zero value. A port whose optionality cannot be represented is never silently coerced.
|
|
859
|
-
*/
|
|
1078
|
+
/** Go type for an input port's declared type (scalar → concrete; array+elemType → []ElemT; else Value). */
|
|
860
1079
|
function inputPortGoType(schema, plan, where = "input port") {
|
|
861
1080
|
const ref = inputPortTypeRef(schema, plan);
|
|
862
1081
|
if (ref !== undefined)
|
|
863
1082
|
return renderTypeRef(ref);
|
|
864
1083
|
if (inputPortIsOptional(schema))
|
|
865
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Go lowering, so "absent" has no native representation
|
|
1084
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Go lowering, so "absent" has no native representation. Declare an elemType for an array/map, or use a natively-lowerable scalar.`);
|
|
866
1085
|
return `${PKG}.Value`;
|
|
867
1086
|
}
|
|
868
|
-
/** The scalar kind
|
|
869
|
-
* An OPTIONAL port is deliberately NOT a scalar kind here: its native type is `*T`, and the callers of
|
|
870
|
-
* this function build REQUIRED scalar slots (`in.<Field>` read straight into a non-opt port field).
|
|
871
|
-
* Answering "int64" for an `{opt:"int"}` port is exactly how "absent" would silently become `0`. */
|
|
1087
|
+
/** The scalar kind an input port declares, or undefined if it is not a native scalar. */
|
|
872
1088
|
function inputScalarKind(schema) {
|
|
873
|
-
if (inputPortIsOptional(schema))
|
|
874
|
-
return undefined;
|
|
875
1089
|
switch (schema?.type) {
|
|
876
1090
|
case "string":
|
|
877
1091
|
case "literal": // a `"literal"` union is a constrained string (see inputPortGoType).
|
|
@@ -896,6 +1110,8 @@ function emitInStruct(comp, plan) {
|
|
|
896
1110
|
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
|
|
897
1111
|
}
|
|
898
1112
|
const names = ports.map((p) => goFieldName(p));
|
|
1113
|
+
// #139: inputPortGoType reads the inputPortTypeRef SSoT — an OPTIONAL port is `*T` (absent → nil); an
|
|
1114
|
+
// optional whose inner type has no native lowering FAILS CLOSED (never a boxed Value escape).
|
|
899
1115
|
const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`));
|
|
900
1116
|
const nameW = Math.max(0, ...names.map((n) => n.length));
|
|
901
1117
|
const typeW = Math.max(0, ...types.map((t) => t.length));
|
|
@@ -937,15 +1153,6 @@ function emitInDecoder(comp, plan) {
|
|
|
937
1153
|
const scalar = inputScalarKind(schema);
|
|
938
1154
|
const et = schema.elemType;
|
|
939
1155
|
lines.push(`\tif v, ok := input.Get(${JSON.stringify(p)}); ok {`);
|
|
940
|
-
if (inputPortIsOptional(schema)) {
|
|
941
|
-
// An OPTIONAL port decodes into its native *T. materializeExpr's opt de-box IS the wire rule:
|
|
942
|
-
// a nil Value (null) → nil (absent), any other value → a pointer to the materialized inner. A key
|
|
943
|
-
// that never appears leaves the struct's zero value (nil) — the same absent value. Checked FIRST:
|
|
944
|
-
// optionality wraps every inner type (an optional array is *[]T, not []T).
|
|
945
|
-
lines.push(`\t\tin.${field} = ${goTypedInternals.materializeExpr("v", inputPortTypeRef(schema, plan), plan)}`);
|
|
946
|
-
lines.push(`\t}`);
|
|
947
|
-
continue;
|
|
948
|
-
}
|
|
949
1156
|
if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
950
1157
|
// #108: decode the boxed Value array into the native []ElemT (test glue — materialize each element
|
|
951
1158
|
// via the typed materializer; a non-array or non-conforming element is a fail-closed decode error).
|
|
@@ -976,11 +1183,9 @@ function emitInDecoder(comp, plan) {
|
|
|
976
1183
|
lines.push(`\t\tin.${field} = v`);
|
|
977
1184
|
}
|
|
978
1185
|
else {
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
lines.push(`\t\
|
|
982
|
-
lines.push(`\t\t}`);
|
|
983
|
-
lines.push(`\t\tin.${field} = t`);
|
|
1186
|
+
// #139: materialize via the inputPortTypeRef SSoT so an OPTIONAL scalar reads as *T (present → &v);
|
|
1187
|
+
// an absent optional never enters this block (Get !ok), leaving the field nil (not a required zero).
|
|
1188
|
+
lines.push(`\t\tin.${field} = ${goTypedInternals.materializeExpr("v", inputPortTypeRef(schema, plan), plan)}`);
|
|
984
1189
|
}
|
|
985
1190
|
lines.push(`\t}`);
|
|
986
1191
|
}
|
|
@@ -988,17 +1193,25 @@ function emitInDecoder(comp, plan) {
|
|
|
988
1193
|
lines.push(`}`);
|
|
989
1194
|
return lines.join("\n");
|
|
990
1195
|
}
|
|
1196
|
+
/** A ref resolver for native lowering: given a ref head + remaining field path, return a native scalar
|
|
1197
|
+
* Go expression (typed input field / prior-node typed struct scalar / element binding field), or null
|
|
1198
|
+
* when the ref does not resolve to a REQUIRED (non-opt) native scalar (then the caller falls back to
|
|
1199
|
+
* the nvVE Value path — which keeps the runtime null/opt/TYPE_MISMATCH semantics). */
|
|
991
1200
|
/**
|
|
992
|
-
* emitPortInit — the port field
|
|
993
|
-
*
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
*
|
|
1201
|
+
* emitPortInit — emit the port field build for one port as a FULLY NATIVE Go expression (bc#90 /
|
|
1202
|
+
* runtime-free). Every covered port lowers to a concrete Go value assigned straight into the ports
|
|
1203
|
+
* struct field — NO `dslcontracts.Value`, NO nvVE helper, NO `.(T)` type-assert:
|
|
1204
|
+
* - a static string array (projection) → `[]string{"a", "b", ...}` (change #2);
|
|
1205
|
+
* - a bare int/float number literal (e.g. Query `limit: 20`) → `int64(20)` / `float64(...)`;
|
|
1206
|
+
* - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
|
|
1207
|
+
* A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered
|
|
1208
|
+
* plane — a boxed escape would re-introduce the bc-runtime coupling this module removes). Returns the
|
|
1209
|
+
* struct field initializer (`Field: <expr>`). `resolveRef` resolves a ref head for the scalar path.
|
|
997
1210
|
*/
|
|
998
1211
|
function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
|
|
999
1212
|
const c = compilePortNode(expr, comp, plan, typedNodes, isPrior, `port '${pn}'`, opts);
|
|
1000
1213
|
if (c.stmts.length > 0)
|
|
1001
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): port '${pn}' ${JSON.stringify(expr)} lowers to a FALLIBLE expression (one that can raise INT_OVERFLOW / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS). Such an expression lowers to STATEMENTS carrying an early error-return, and a port is built inline in the ports-struct literal — a position that cannot host them.
|
|
1214
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): port '${pn}' ${JSON.stringify(expr)} lowers to a FALLIBLE expression (one that can raise INT_OVERFLOW / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS). Such an expression lowers to STATEMENTS carrying an early error-return, and a port is built inline in the ports-struct literal — a position that cannot host them. Use a non-fallible port expression.`);
|
|
1002
1215
|
return `${goPortFieldName(pn)}: ${c.expr}`;
|
|
1003
1216
|
}
|
|
1004
1217
|
/**
|
|
@@ -1022,7 +1235,7 @@ function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
|
|
|
1022
1235
|
*
|
|
1023
1236
|
* NO RunPlan / []OpSpec / planSpec walk: the staging, the bound, and which ops are parallel are baked.
|
|
1024
1237
|
*/
|
|
1025
|
-
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) {
|
|
1026
1239
|
const lines = [];
|
|
1027
1240
|
const ids = stage.map((i) => comp.body[i].id);
|
|
1028
1241
|
lines.push(`\t// ── bc#87 real-concurrency stage {${ids.map((x) => JSON.stringify(x)).join(", ")}} — static parallel`);
|
|
@@ -1059,9 +1272,10 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1059
1272
|
const node = comp.body[i];
|
|
1060
1273
|
const s = sanitize(node.id);
|
|
1061
1274
|
const structName = portsStructName(comp.name, comp.body[i].id);
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
lines.push(`\t\tvar
|
|
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}`);
|
|
1278
|
+
lines.push(`\t\tvar row_${s}Err error`);
|
|
1065
1279
|
lines.push(`\t\trun_${s} := false`);
|
|
1066
1280
|
lines.push(`\t\tvar ps_${s} ${structName}`);
|
|
1067
1281
|
lines.push(`\t\t_ = ps_${s}`);
|
|
@@ -1133,32 +1347,43 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1133
1347
|
lines.push(`\t\t\t\tdefer wg.Done()`);
|
|
1134
1348
|
lines.push(`\t\t\t\tdefer func() { <-sem }()`);
|
|
1135
1349
|
const boundArg = node.bindField !== undefined ? `bound_${s}` : "nil";
|
|
1136
|
-
lines.push(`\t\t\t\
|
|
1350
|
+
lines.push(`\t\t\t\twire_${s}, row_${s}Err = handlers.${nodeMethodName(comp.name, node.id)}(ps_${s}, ${boundArg})`);
|
|
1137
1351
|
lines.push(`\t\t\t}()`);
|
|
1138
1352
|
lines.push(`\t\t}`);
|
|
1139
1353
|
}
|
|
1140
1354
|
lines.push(`\t\twg.Wait()`);
|
|
1141
|
-
// 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).
|
|
1142
1359
|
for (const i of refMembers) {
|
|
1143
1360
|
const node = comp.body[i];
|
|
1144
1361
|
const meta = metas[i];
|
|
1145
1362
|
const s = sanitize(node.id);
|
|
1146
1363
|
const ref = typedNodes.get(node.id);
|
|
1147
|
-
const oc = `row_${s}`;
|
|
1148
1364
|
lines.push(`\t\tif run_${s} {`);
|
|
1149
|
-
lines.push(`\t\t\tif !${oc}Resolved {`, `\t\t\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${node.component}' has no handler (fail-closed)"`)}`, `\t\t\t}`);
|
|
1150
1365
|
if (meta.policy === "continue") {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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}`);
|
|
1154
1380
|
lines.push(`\t\t\t}`);
|
|
1155
1381
|
}
|
|
1156
1382
|
else {
|
|
1157
|
-
|
|
1158
|
-
lines.push(`\t\t\
|
|
1159
|
-
lines.push(`\t\t\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under '${policy}: "+${oc}.Err`)}`);
|
|
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)`);
|
|
1160
1385
|
lines.push(`\t\t\t}`);
|
|
1161
|
-
lines.push(
|
|
1386
|
+
lines.push(renderResultDeBox(ref, typedLocal(node.id), `wire_${s}`, node.id, plan, "\t\t\t", returnZeroFail(zeroOut)));
|
|
1162
1387
|
lines.push(`\t\t\tproduced_${s} = true`);
|
|
1163
1388
|
}
|
|
1164
1389
|
lines.push(`\t\t}`);
|
|
@@ -1179,7 +1404,7 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1179
1404
|
* consumer (graphddb) drives it and keeps the struct native. A Value observation is produced ONLY
|
|
1180
1405
|
* by the separate serialize wrapper (run_native_raw_<comp>, below), the single outer Value boundary.
|
|
1181
1406
|
*/
|
|
1182
|
-
function emitNativeRawRunner(comp, plan, flags) {
|
|
1407
|
+
function emitNativeRawRunner(comp, plan, flags, wire) {
|
|
1183
1408
|
const ep = execPlan(comp);
|
|
1184
1409
|
const order = ep.order;
|
|
1185
1410
|
const cplan = buildComponentPlan(comp, goStraightlineInternals.dialect, "scope");
|
|
@@ -1200,6 +1425,9 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1200
1425
|
const outRef = comp.outputType !== undefined ? deriveTypeRef(comp.outputType, plan) : undefined;
|
|
1201
1426
|
const outStructType = outRef ? renderTypeRef(outRef) : `${PKG}.Value`;
|
|
1202
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 };
|
|
1203
1431
|
const lines = [];
|
|
1204
1432
|
lines.push(`// ${runnerName(comp.name)} — the STRUCT-RETURNING combined read (bc#77/#87): the fully`);
|
|
1205
1433
|
lines.push(`// de-plumbed path. Ports are a native struct (direct field assignment); the handler`);
|
|
@@ -1213,11 +1441,12 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1213
1441
|
lines.push(`// index order so the observed value / op multiset / failure precedence byte-match run_behavior`);
|
|
1214
1442
|
lines.push(`// (the goroutine spawn is the ONLY runtime element — the WHAT is baked). The output is a typed`);
|
|
1215
1443
|
lines.push(`// struct/value assembled by struct literal + field access — the consumer keeps it native.`);
|
|
1216
|
-
// FULLY CONCRETE
|
|
1217
|
-
//
|
|
1218
|
-
//
|
|
1219
|
-
//
|
|
1220
|
-
|
|
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) {`);
|
|
1221
1450
|
lines.push(`\t_ = in`);
|
|
1222
1451
|
for (const k of order.keys()) {
|
|
1223
1452
|
const node = comp.body[order[k]];
|
|
@@ -1241,7 +1470,7 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1241
1470
|
// ascending-sorted stages), so we emit the block at the first member and skip the rest.
|
|
1242
1471
|
const stage = ep.parallelStageOf.get(i);
|
|
1243
1472
|
if (stage !== undefined && stage[0] === i) {
|
|
1244
|
-
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));
|
|
1245
1474
|
k += stage.length - 1;
|
|
1246
1475
|
continue;
|
|
1247
1476
|
}
|
|
@@ -1317,22 +1546,35 @@ function emitNativeRawRunner(comp, plan, flags) {
|
|
|
1317
1546
|
inits.push(emitPortInit(pn, node.ports[pn], comp, plan, typedNodes, (h) => priorNodeAt(h, k)));
|
|
1318
1547
|
}
|
|
1319
1548
|
lines.push(`${indent}ports_${s} := ${structName}{${inits.join(", ")}}`);
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
const copy = (ind) => emitRowCopy(typedLocal(node.id), oc, ref, plan, ind.length);
|
|
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).
|
|
1324
1552
|
if (meta.policy === "continue") {
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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}`);
|
|
1328
1569
|
lines.push(`${indent}}`);
|
|
1329
1570
|
}
|
|
1330
1571
|
else {
|
|
1331
|
-
const
|
|
1332
|
-
lines.push(`${indent}
|
|
1333
|
-
lines.push(`${indent}
|
|
1572
|
+
const policyLit = JSON.stringify(meta.policy === "retry" ? "retry" : "fail");
|
|
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)`);
|
|
1334
1576
|
lines.push(`${indent}}`);
|
|
1335
|
-
lines.push(
|
|
1577
|
+
lines.push(renderResultDeBox(ref, typedLocal(node.id), `wire_${s}`, node.id, plan, indent, returnZeroFail(zeroOut), seqCtr));
|
|
1336
1578
|
lines.push(`${indent}produced_${s} = true`);
|
|
1337
1579
|
}
|
|
1338
1580
|
for (const c of closers)
|
|
@@ -1408,7 +1650,8 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1408
1650
|
if (!hasGuard)
|
|
1409
1651
|
return null;
|
|
1410
1652
|
const resolveHead = goNativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding: { name: asName, ref: overElemRef, plan }, asBase: elemVar });
|
|
1411
|
-
|
|
1653
|
+
const be = makeGoExprBackend(zeroOut, resolveHead, plan);
|
|
1654
|
+
return new NativeExprCompiler(be).compileBool(m.when);
|
|
1412
1655
|
};
|
|
1413
1656
|
// emit the per-element native ports struct build (shared; `il` is the indent inside the element
|
|
1414
1657
|
// loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
|
|
@@ -1430,7 +1673,13 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1430
1673
|
// over collection (typed field access to the parent node's arr — no serialize).
|
|
1431
1674
|
lines.push(`${indent}over_${s} := ${overExpr}`);
|
|
1432
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.
|
|
1433
1680
|
const elemRowRef = mapElemRowRef(node, arrRef, plan);
|
|
1681
|
+
const elemRowGo = renderTypeRef(elemRowRef);
|
|
1682
|
+
const renderElemDeBox = (target, src, il) => renderResultDeBox(elemRowRef, target, src, node.id, plan, il, returnZeroFail(zeroOut));
|
|
1434
1683
|
// emit the per-element guard keep-gate (#108). `il` = indent, `elemVar` = the over element loop var.
|
|
1435
1684
|
// On skip: batched → `continue` (element excluded from items + keptOver); non-batched → `continue`.
|
|
1436
1685
|
const emitGuard = (il, elemVar) => {
|
|
@@ -1447,9 +1696,9 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1447
1696
|
};
|
|
1448
1697
|
if (batched) {
|
|
1449
1698
|
// BATCHED (#86 pt2 into / #93 shape 3 no-into / #108 guarded no-into): build kept-element ports,
|
|
1450
|
-
// dispatch ONCE. The concrete Node_* returns
|
|
1699
|
+
// dispatch ONCE. The concrete Node_* returns the aligned per-element WIRES ([]WireValue) or an error.
|
|
1451
1700
|
// With a guard, `items` (and, for into, `keptOver`) hold only kept elements — matching run_behavior's
|
|
1452
|
-
// 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.
|
|
1453
1702
|
lines.push(`${indent}items_${s} := make([]${structName}, 0, len(over_${s}))`);
|
|
1454
1703
|
if (hasInto)
|
|
1455
1704
|
lines.push(`${indent}keptOver_${s} := make([]${overElemGo}, 0, len(over_${s}))`);
|
|
@@ -1465,22 +1714,24 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1465
1714
|
// empty items => empty result (handler not called), matching run_behavior.
|
|
1466
1715
|
lines.push(`${indent}if len(items_${s}) > 0 {`);
|
|
1467
1716
|
lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: items_${s}}`);
|
|
1468
|
-
lines.push(`${indent}\tmo_${s}, mo_${s}
|
|
1469
|
-
lines.push(`${indent}\tif
|
|
1470
|
-
lines.push(`${indent}\tif mo_${s}
|
|
1471
|
-
lines.push(`${indent}\tif len(mo_${s}.Rows) != len(items_${s}) {`, `${indent}\t\treturn ${zeroOut}, ${goErr("MAP_BATCH_RESULT_MISMATCH", `fmt.Sprintf("map '${node.id}': batched handler must return a list aligned to items (want %d)", len(items_${s}))`)}`, `${indent}\t}`);
|
|
1717
|
+
lines.push(`${indent}\tmo_${s}, mo_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
|
|
1718
|
+
lines.push(`${indent}\tif mo_${s}Err != nil {`, `${indent}\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, "fail", mo_${s}Err)`, `${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}`);
|
|
1472
1720
|
if (hasInto) {
|
|
1473
|
-
// #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).
|
|
1474
1723
|
lines.push(`${indent}\tfor mk_${s} := range keptOver_${s} {`);
|
|
1475
|
-
lines.push(`${indent}\t\
|
|
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})`);
|
|
1476
1727
|
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1477
1728
|
lines.push(`${indent}\t}`);
|
|
1478
1729
|
}
|
|
1479
1730
|
else {
|
|
1480
|
-
// #93 shape 3 / #108:
|
|
1481
|
-
lines.push(`${indent}\tfor 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}++ {`);
|
|
1482
1733
|
lines.push(`${indent}\t\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
|
|
1483
|
-
lines.push(
|
|
1734
|
+
lines.push(renderElemDeBox(`el_${s}`, `mo_${s}[mk_${s}]`, `${indent}\t\t`));
|
|
1484
1735
|
lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1485
1736
|
lines.push(`${indent}\t}`);
|
|
1486
1737
|
}
|
|
@@ -1489,7 +1740,7 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1489
1740
|
else {
|
|
1490
1741
|
// #93 shape 2 / #108: NON-batched map — dispatch the child handler ONCE PER KEPT ELEMENT (N physical
|
|
1491
1742
|
// requests, matching run_behavior's per-element loop). A guarded element that fails `when` is skipped
|
|
1492
|
-
// (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).
|
|
1493
1744
|
lines.push(`${indent}for mk_${s} := range over_${s} {`);
|
|
1494
1745
|
lines.push(`${indent}\t${elemLocal(s)} := over_${s}[mk_${s}]`);
|
|
1495
1746
|
for (const l of emitGuard(indent + "\t", elemLocal(s)))
|
|
@@ -1499,15 +1750,27 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
|
|
|
1499
1750
|
// per-element dispatch (ONE physical request per element). run_behavior passes the over element
|
|
1500
1751
|
// as the bound value; the element data is fully conveyed by the native ports struct here, so bound
|
|
1501
1752
|
// stays nil. Byte-equivalence holds because the bound value only feeds handler-internal logic.
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1753
|
+
// Element Error Policy (scp-error.md): `skip` drops the failing element and proceeds (order is
|
|
1754
|
+
// preserved; the leaf keeps the Error Value it produced). `error` (default) promotes the element
|
|
1755
|
+
// Failure to the map's Component Failure, carrying the leaf's detail through.
|
|
1756
|
+
lines.push(`${indent}\tmo_${s}, mo_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(ep_${s}, nil)`);
|
|
1757
|
+
if (m.elementPolicy === "skip") {
|
|
1758
|
+
lines.push(`${indent}\tif mo_${s}Err != nil {`, `${indent}\t\tcontinue`, `${indent}\t}`);
|
|
1759
|
+
}
|
|
1760
|
+
else {
|
|
1761
|
+
lines.push(`${indent}\tif mo_${s}Err != nil {`, `${indent}\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, "fail", mo_${s}Err)`, `${indent}\t}`);
|
|
1762
|
+
}
|
|
1763
|
+
// the element's wire is de-boxed strictly INLINE. A de-box mismatch fails closed regardless of
|
|
1764
|
+
// elementPolicy (≡ the interpreter's whole-node outType throw); elementPolicy governs the handler's
|
|
1765
|
+
// own leaf failures only (above).
|
|
1505
1766
|
if (hasInto) {
|
|
1506
|
-
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})`);
|
|
1507
1770
|
}
|
|
1508
1771
|
else {
|
|
1509
1772
|
lines.push(`${indent}\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
|
|
1510
|
-
lines.push(
|
|
1773
|
+
lines.push(renderElemDeBox(`el_${s}`, `mo_${s}`, `${indent}\t`));
|
|
1511
1774
|
}
|
|
1512
1775
|
lines.push(`${indent}\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
|
|
1513
1776
|
lines.push(`${indent}}`);
|
|
@@ -1578,19 +1841,22 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
|
|
|
1578
1841
|
// empty id-list => empty connection (handler not called), matching run_behavior.
|
|
1579
1842
|
lines.push(`${indent}if len(pi_${s}) > 0 {`);
|
|
1580
1843
|
lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: pi_${s}}`);
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
lines.push(`${indent}\
|
|
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.
|
|
1847
|
+
lines.push(`${indent}\tfo_${s}, fo_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
|
|
1848
|
+
lines.push(`${indent}\tif fo_${s}Err != nil {`, `${indent}\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, "fail", fo_${s}Err)`, `${indent}\t}`);
|
|
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}`);
|
|
1585
1850
|
// ── THE ONE dedupe/drop (mirrors fanoutDedupDrop verbatim): first-seen dedupe by dedupeKey field;
|
|
1586
1851
|
// drop dangling (zero dedupeKey ≡ interpreter's null/absent-key body); implicitSource strip is
|
|
1587
1852
|
// structural (the elem type already omits it). cursor is always nil (connection wrap).
|
|
1588
|
-
lines.push(`${indent}\tseen_${s} := make(map[string]struct{}, len(fo_${s}
|
|
1589
|
-
lines.push(`${indent}\titems_${s} = make([]${elemGo}, 0, len(fo_${s}
|
|
1590
|
-
lines.push(`${indent}\tfor fi_${s} := range fo_${s}
|
|
1591
|
-
lines.push(`${indent}\t\tbody_${s} := fo_${s}.Rows[fi_${s}]`);
|
|
1853
|
+
lines.push(`${indent}\tseen_${s} := make(map[string]struct{}, len(fo_${s}))`);
|
|
1854
|
+
lines.push(`${indent}\titems_${s} = make([]${elemGo}, 0, len(fo_${s}))`);
|
|
1855
|
+
lines.push(`${indent}\tfor fi_${s} := range fo_${s} {`);
|
|
1592
1856
|
lines.push(`${indent}\t\tvar el_${s} ${elemGo}`);
|
|
1593
|
-
lines.push(
|
|
1857
|
+
lines.push(`${indent}\t\tif fo_${s}[fi_${s}] != nil {`);
|
|
1858
|
+
lines.push(renderResultDeBox(elemRef, `el_${s}`, `(*fo_${s}[fi_${s}])`, node.id, plan, `${indent}\t\t\t`, returnZeroFail(zeroOut)));
|
|
1859
|
+
lines.push(`${indent}\t\t}`);
|
|
1594
1860
|
lines.push(`${indent}\t\tkey_${s} := el_${s}.${dkGo}`);
|
|
1595
1861
|
if (f.drop === "dangling") {
|
|
1596
1862
|
// dangling: a body whose dedupeKey is the zero value (empty string) is dropped.
|
|
@@ -1609,15 +1875,12 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
|
|
|
1609
1875
|
lines.push(c);
|
|
1610
1876
|
return lines.join("\n");
|
|
1611
1877
|
}
|
|
1612
|
-
/**
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1615
|
-
* so an OPTIONAL port resolves as `{opt:…}` (native `*T`) and the compiler can see its optionality: a
|
|
1616
|
-
* null-compare becomes a real presence test and a coalesce a real deref-or-default, while a
|
|
1617
|
-
* REQUIRED-scalar slot fed by an opt fails closed. Go values are read directly off the input struct (no
|
|
1618
|
-
* ownership ceremony); a type with no native lowering → null (fail-closed).
|
|
1619
|
-
*/
|
|
1878
|
+
/** #108: resolve an INPUT port head for a native-expr ref (cond `if` / map `when` / branches). A scalar
|
|
1879
|
+
* port → `in.<Field>` of the scalar type; an array port WITH elemType → `in.<Field>` of `[]ElemT` (so
|
|
1880
|
+
* `len(in.<Field>)` works); anything else → null (fail-closed). */
|
|
1620
1881
|
function goInputHeadRef(head, comp, plan) {
|
|
1882
|
+
// #139: the port's declared type comes from the inputPortTypeRef SSoT, so an OPTIONAL port resolves as
|
|
1883
|
+
// {opt:…} (native *T) and the compiler sees its optionality; a type with no native lowering → null.
|
|
1621
1884
|
const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
|
|
1622
1885
|
if (ref === undefined)
|
|
1623
1886
|
return null;
|
|
@@ -1635,7 +1898,11 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut)
|
|
|
1635
1898
|
const c = node.cond;
|
|
1636
1899
|
const s = sanitize(node.id);
|
|
1637
1900
|
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1638
|
-
const resolveHead =
|
|
1901
|
+
const resolveHead = (head) => {
|
|
1902
|
+
if (priorHere(head))
|
|
1903
|
+
return { expr: typedLocal(head), ref: typedNodes.get(head) };
|
|
1904
|
+
return goInputHeadRef(head, comp, plan);
|
|
1905
|
+
};
|
|
1639
1906
|
const be = makeGoExprBackend(zeroOut, resolveHead, plan);
|
|
1640
1907
|
const compiler = new NativeExprCompiler(be);
|
|
1641
1908
|
const cond = compiler.compileBool(c.if);
|
|
@@ -1866,14 +2133,11 @@ function makeGoExprBackend(zeroOut, resolveHead, plan) {
|
|
|
1866
2133
|
// opt (`*T`) null-presence test: present = `(expr != nil)`, absent = `(expr == nil)`.
|
|
1867
2134
|
optIsSome: (expr) => `(${expr} != nil)`,
|
|
1868
2135
|
optIsNone: (expr) => `(${expr} == nil)`,
|
|
1869
|
-
// opt (`*T`) defaulted to a PURE default
|
|
1870
|
-
//
|
|
1871
|
-
// pointer ONCE, deref it when present, else yield the default. The default sits in the else branch,
|
|
1872
|
-
// so it is evaluated only when absent — run_behavior's lazy right side exactly.
|
|
2136
|
+
// opt (`*T`) defaulted to a PURE default — an immediately-invoked func literal: bind the pointer ONCE,
|
|
2137
|
+
// deref when present, else yield the default (evaluated only when absent — run_behavior's lazy right).
|
|
1873
2138
|
optUnwrapOr: (optExpr, defaultExpr, innerTy) => `func() ${innerTy} { if v := ${optExpr}; v != nil { return *v }; return ${defaultExpr} }()`,
|
|
1874
|
-
// opt defaulted to a FALLIBLE default — a statement if/else
|
|
1875
|
-
//
|
|
1876
|
-
// would swallow (it would return from the literal). Only the nil branch evaluates the default.
|
|
2139
|
+
// opt defaulted to a FALLIBLE default — a statement if/else (NOT a func literal): the default's hoisted
|
|
2140
|
+
// `return zeroOut, err` early returns must leave the ENCLOSING fn. Only the nil branch evaluates it.
|
|
1877
2141
|
optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
|
|
1878
2142
|
`var ${temp} ${ty}`,
|
|
1879
2143
|
`if ${bindTemp} := ${optExpr}; ${bindTemp} != nil {`,
|
|
@@ -2037,17 +2301,23 @@ var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name
|
|
|
2037
2301
|
* mismatch Sprintf, `strings` for a concat builder, `sync` for a parallel stage). ExpectedSpecVersions
|
|
2038
2302
|
* stays as a local `var` for provenance, but the old `dslcontracts.SpecVersions` skew `init()` is GONE
|
|
2039
2303
|
* (change #5 — there is no runtime to skew against once the module is runtime-free). */
|
|
2040
|
-
function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath) {
|
|
2304
|
+
function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath, needStrconv = false, wireImport) {
|
|
2041
2305
|
const sv = ctx.specVersions;
|
|
2042
2306
|
const imports = [];
|
|
2043
2307
|
if (needFmt)
|
|
2044
2308
|
imports.push(`\t"fmt"`);
|
|
2045
2309
|
if (needMath)
|
|
2046
2310
|
imports.push(`\t"math"`);
|
|
2311
|
+
if (needStrconv)
|
|
2312
|
+
imports.push(`\t"strconv"`);
|
|
2047
2313
|
if (needStrings)
|
|
2048
2314
|
imports.push(`\t"strings"`);
|
|
2049
2315
|
if (needSync)
|
|
2050
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)}`);
|
|
2051
2321
|
const importBlock = imports.length > 0 ? `\nimport (\n${imports.join("\n")}\n)\n` : "";
|
|
2052
2322
|
return `// GENERATED by behavior-contracts (bc#90, go-typed-native — the RUNTIME-FREE covered read de-box) — DO NOT EDIT.
|
|
2053
2323
|
// Input: portable component-graph IR only. Every component here is a COVERED READ (sequential typed
|
|
@@ -2063,29 +2333,33 @@ var ExpectedSpecVersions = map[string]int64{"behavior": ${sv.behavior}, "express
|
|
|
2063
2333
|
}
|
|
2064
2334
|
// ── module assembly ──────────────────────────────────────────────────────────────────
|
|
2065
2335
|
function emit(ctx) {
|
|
2066
|
-
// The type plan is the SSoT the coverage gate + the port lowering both read (the gate ASKS the port
|
|
2067
|
-
// lowering whether a port lowers), so it is built up-front — before the coverage checks.
|
|
2068
|
-
const plan = buildTypePlan(ctx.ir);
|
|
2069
2336
|
// Fail closed if a covered read SHAPE lacks the typed annotations (no silent re-box).
|
|
2070
2337
|
for (const c of ctx.ir.components)
|
|
2071
|
-
assertTypedCoverage(c
|
|
2072
|
-
const native = ctx.ir.components.filter(
|
|
2073
|
-
const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c
|
|
2338
|
+
assertTypedCoverage(c);
|
|
2339
|
+
const native = ctx.ir.components.filter(isNativeRaw);
|
|
2340
|
+
const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
|
|
2341
|
+
const plan = buildTypePlan(ctx.ir);
|
|
2074
2342
|
const decls = emitTypeDecls(plan);
|
|
2075
2343
|
// #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler when it emits
|
|
2076
2344
|
// a fallible checked-arith helper call — determines whether the helper library + `math` import ship).
|
|
2077
2345
|
goExprUsed.helpers = false;
|
|
2346
|
+
const wire = resolveGoWire(ctx);
|
|
2078
2347
|
// Emit the runners first so the nv* helper usage flags are known.
|
|
2079
2348
|
const flags = { bind: false, field: false, cat: false, arr: false };
|
|
2080
2349
|
const structs = [];
|
|
2081
2350
|
const inStructs = [];
|
|
2082
|
-
const rowStructs = [];
|
|
2083
|
-
const handlerIfaces = [];
|
|
2084
2351
|
const elemCopiers = [];
|
|
2085
2352
|
const runnerCodes = [];
|
|
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)));
|
|
2086
2356
|
for (const c of native) {
|
|
2087
2357
|
inStructs.push(emitInStruct(c, plan));
|
|
2088
|
-
const typedNodes =
|
|
2358
|
+
const typedNodes = new Map();
|
|
2359
|
+
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
2360
|
+
for (const n of c.body)
|
|
2361
|
+
if (!isControlGate(n))
|
|
2362
|
+
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan)); // #125: control-gate typeless; map は produced-array ref に正規化
|
|
2089
2363
|
for (const n of c.body) {
|
|
2090
2364
|
if ("fanout" in n) {
|
|
2091
2365
|
// v3: a fanout node emits a per-id element ports struct + a batch struct (like a batched map).
|
|
@@ -2101,17 +2375,14 @@ function emit(ctx) {
|
|
|
2101
2375
|
}
|
|
2102
2376
|
else {
|
|
2103
2377
|
// #129: pass typedNodes so a leaf array port fed by a prior node's arr outType types as []ElemT.
|
|
2104
|
-
structs.push(emitPortsStruct(c, n, plan, typedNodes));
|
|
2378
|
+
structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes));
|
|
2105
2379
|
}
|
|
2106
2380
|
}
|
|
2107
|
-
//
|
|
2108
|
-
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
2109
|
-
handlerIfaces.push(emitHandlerInterface(c, typedNodes, plan));
|
|
2110
|
-
// 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).
|
|
2111
2382
|
const mm = emitMapElemMaterializers(c, typedNodes, plan);
|
|
2112
2383
|
if (mm)
|
|
2113
2384
|
elemCopiers.push(mm);
|
|
2114
|
-
const r = emitNativeRawRunner(c, plan, flags);
|
|
2385
|
+
const r = emitNativeRawRunner(c, plan, flags, wire);
|
|
2115
2386
|
runnerCodes.push(r.code);
|
|
2116
2387
|
}
|
|
2117
2388
|
const structSurface = emitStructSurface(native);
|
|
@@ -2130,7 +2401,7 @@ function emit(ctx) {
|
|
|
2130
2401
|
// minimal, runtime-free header.
|
|
2131
2402
|
if (nonNative.length > 0) {
|
|
2132
2403
|
const rejects = nonNative
|
|
2133
|
-
.map((c) => ` - component '${c.name}': ${coverageRejectReason(c
|
|
2404
|
+
.map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
|
|
2134
2405
|
.join("\n");
|
|
2135
2406
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (native codegen) surface: ${nonNative.length} component(s) are NOT a covered native read:\n${rejects}\n\n` +
|
|
2136
2407
|
`The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
|
|
@@ -2140,30 +2411,32 @@ function emit(ctx) {
|
|
|
2140
2411
|
// #108: `math` is imported iff the native-expr compiler emitted a fallible helper (bcExpr_modF uses
|
|
2141
2412
|
// math.Mod). The helper library itself is baked into the module only when used (goExprUsed.helpers).
|
|
2142
2413
|
const needMath = goExprUsed.helpers;
|
|
2143
|
-
|
|
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);
|
|
2144
2417
|
const banner = `// COMBINED READ layer (bc#90 — the RUNTIME-FREE read de-box; STRUCT-ONLY, FULLY-NATIVE surface). A
|
|
2145
2418
|
// covered read is a strictly-sequential typed componentRef chain (point reads + relationKind:single
|
|
2146
2419
|
// children), emitted ONLY here: each node builds a native ports struct by direct field assignment (every
|
|
2147
2420
|
// port is a CONCRETE Go value — string / bool / []string projection / int64 limit — NO boxed Value, NO
|
|
2148
|
-
// PortReader accessor); the
|
|
2149
|
-
//
|
|
2150
|
-
//
|
|
2151
|
-
//
|
|
2152
|
-
//
|
|
2153
|
-
//
|
|
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
|
|
2154
2428
|
// decision is made from the real parent value — relationSingle / connection converge with run_behavior
|
|
2155
|
-
// (fixes #323/#74). The exposed API is the STRUCT-returning RunNativeRawStruct_<comp> (the consumer
|
|
2156
|
-
//
|
|
2157
|
-
//
|
|
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).`;
|
|
2158
2432
|
const sections = [
|
|
2159
2433
|
banner,
|
|
2160
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}`,
|
|
2161
2435
|
decls ? `// Typed struct declarations (outType-derived; hash-dedup — shared type plan).\n${decls}` : undefined,
|
|
2162
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")}`,
|
|
2163
|
-
|
|
2164
|
-
? `//
|
|
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)}`
|
|
2165
2439
|
: undefined,
|
|
2166
|
-
`// Per-component CONCRETE handler interfaces (one typed Node_* method per covered node).\n${handlerIfaces.join("\n\n")}`,
|
|
2167
2440
|
elemCopiers.length
|
|
2168
2441
|
? `// Augmented map-element copiers (over element fields + the concrete into row — pure assignment).\n${elemCopiers.join("\n\n")}`
|
|
2169
2442
|
: undefined,
|
|
@@ -2188,8 +2461,8 @@ function emit(ctx) {
|
|
|
2188
2461
|
*/
|
|
2189
2462
|
export function goTypedNativeObserve(ir, runtimeImport) {
|
|
2190
2463
|
const components = ir.components;
|
|
2464
|
+
const native = components.filter(isNativeRaw);
|
|
2191
2465
|
const plan = buildTypePlan(ir);
|
|
2192
|
-
const native = components.filter((c) => isNativeRaw(c, plan));
|
|
2193
2466
|
const serializers = emitSerializers(plan);
|
|
2194
2467
|
const marshallers = goTypedInternals.emitMarshallers(plan);
|
|
2195
2468
|
const mustHelpers = emitMustHelpers(plan);
|
|
@@ -2203,27 +2476,215 @@ export function goTypedNativeObserve(ir, runtimeImport) {
|
|
|
2203
2476
|
// decode the generic *Obj input into the CONCRETE In_<comp> struct (test glue — ONE decode at the
|
|
2204
2477
|
// top, OUTSIDE the covered hot path; a REQUIRED declared input is present so an absent key is a
|
|
2205
2478
|
// fail-closed decode error surfaced here, never on the native runner path).
|
|
2206
|
-
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}`;
|
|
2207
2480
|
})
|
|
2208
2481
|
.join("\n");
|
|
2209
2482
|
// input decoders (TEST glue): generic *Obj -> concrete In_<comp>. Scalar fields read the typed value
|
|
2210
2483
|
// (a `.(T)` here is on the SDK/wire Value in the OBSERVE companion, never in the covered module).
|
|
2211
2484
|
const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
|
|
2212
|
-
//
|
|
2213
|
-
//
|
|
2214
|
-
const
|
|
2215
|
-
|
|
2216
|
-
//
|
|
2217
|
-
type
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
//
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
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.
|
|
2494
|
+
const wireAdapter = anyDeBox
|
|
2495
|
+
? `// ── scripted wire adapter (TEST glue) ──
|
|
2496
|
+
func scriptedWireTag(v ${PKG}.Value) string {
|
|
2497
|
+
switch v.(type) {
|
|
2498
|
+
case nil:
|
|
2499
|
+
return "NULL"
|
|
2500
|
+
case bool:
|
|
2501
|
+
return "BOOL"
|
|
2502
|
+
case int64, float64:
|
|
2503
|
+
return "N"
|
|
2504
|
+
case string:
|
|
2505
|
+
return "S"
|
|
2506
|
+
case []${PKG}.Value:
|
|
2507
|
+
return "L"
|
|
2508
|
+
case *${PKG}.Obj:
|
|
2509
|
+
return "M"
|
|
2510
|
+
default:
|
|
2511
|
+
return "UNKNOWN"
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
func scriptedWireRaw(v ${PKG}.Value) string {
|
|
2516
|
+
switch t := v.(type) {
|
|
2517
|
+
case nil:
|
|
2518
|
+
return "null"
|
|
2519
|
+
case bool:
|
|
2520
|
+
if t {
|
|
2521
|
+
return "true"
|
|
2522
|
+
}
|
|
2523
|
+
return "false"
|
|
2524
|
+
case int64:
|
|
2525
|
+
return strconv.FormatInt(t, 10)
|
|
2526
|
+
case float64:
|
|
2527
|
+
s, _ := ${PKG}.PyFloatRepr(t)
|
|
2528
|
+
return s
|
|
2529
|
+
case string:
|
|
2530
|
+
return t
|
|
2531
|
+
default:
|
|
2532
|
+
return ${PKG}.EncodeValue(v)
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
func probeStringVal(v ${PKG}.Value) StringProbe {
|
|
2537
|
+
if v == nil {
|
|
2538
|
+
return StringProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
|
|
2539
|
+
}
|
|
2540
|
+
if s, ok := v.(string); ok {
|
|
2541
|
+
return StringProbe{Kind: probeGot, Got: s, ActualWireType: "S", Raw: s}
|
|
2542
|
+
}
|
|
2543
|
+
return StringProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
func probeNumberVal(v ${PKG}.Value) NumberProbe {
|
|
2547
|
+
if v == nil {
|
|
2548
|
+
return NumberProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
|
|
2549
|
+
}
|
|
2550
|
+
switch v.(type) {
|
|
2551
|
+
case int64, float64:
|
|
2552
|
+
return NumberProbe{Kind: probeGot, Got: scriptedWireRaw(v), ActualWireType: "N", Raw: scriptedWireRaw(v)}
|
|
2553
|
+
}
|
|
2554
|
+
return NumberProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
func probeBoolVal(v ${PKG}.Value) BoolProbe {
|
|
2558
|
+
if v == nil {
|
|
2559
|
+
return BoolProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
|
|
2560
|
+
}
|
|
2561
|
+
if b, ok := v.(bool); ok {
|
|
2562
|
+
return BoolProbe{Kind: probeGot, Got: b, ActualWireType: "BOOL", Raw: scriptedWireRaw(v)}
|
|
2563
|
+
}
|
|
2564
|
+
return BoolProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
func probeRowVal(v ${PKG}.Value) RowProbe {
|
|
2568
|
+
if v == nil {
|
|
2569
|
+
return RowProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
|
|
2570
|
+
}
|
|
2571
|
+
if o, ok := v.(*${PKG}.Obj); ok {
|
|
2572
|
+
return RowProbe{Kind: probeGot, Got: WireRow{obj: o}, ActualWireType: "M"}
|
|
2573
|
+
}
|
|
2574
|
+
return RowProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
func probeListVal(v ${PKG}.Value) ListProbe {
|
|
2578
|
+
if v == nil {
|
|
2579
|
+
return ListProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
|
|
2580
|
+
}
|
|
2581
|
+
if a, ok := v.([]${PKG}.Value); ok {
|
|
2582
|
+
return ListProbe{Kind: probeGot, Got: WireList{items: a}, ActualWireType: "L"}
|
|
2583
|
+
}
|
|
2584
|
+
return ListProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
|
|
2585
|
+
}
|
|
2586
|
+
|
|
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
|
|
2592
|
+
}
|
|
2593
|
+
|
|
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
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
func (w WireRow) field(name string) (${PKG}.Value, bool) {
|
|
2607
|
+
if w.obj == nil {
|
|
2608
|
+
return nil, false
|
|
2609
|
+
}
|
|
2610
|
+
return w.obj.Get(name)
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
func (w WireRow) Keys() []string {
|
|
2614
|
+
if w.obj == nil {
|
|
2615
|
+
return nil
|
|
2616
|
+
}
|
|
2617
|
+
return w.obj.Keys
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
func (w WireRow) ProbeString(field string) StringProbe {
|
|
2621
|
+
if v, ok := w.field(field); ok {
|
|
2622
|
+
return probeStringVal(v)
|
|
2623
|
+
}
|
|
2624
|
+
return StringProbe{Kind: probeAbsent}
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
func (w WireRow) ProbeNumber(field string) NumberProbe {
|
|
2628
|
+
if v, ok := w.field(field); ok {
|
|
2629
|
+
return probeNumberVal(v)
|
|
2630
|
+
}
|
|
2631
|
+
return NumberProbe{Kind: probeAbsent}
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
func (w WireRow) ProbeBool(field string) BoolProbe {
|
|
2635
|
+
if v, ok := w.field(field); ok {
|
|
2636
|
+
return probeBoolVal(v)
|
|
2637
|
+
}
|
|
2638
|
+
return BoolProbe{Kind: probeAbsent}
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
func (w WireRow) ProbeRow(field string) RowProbe {
|
|
2642
|
+
if v, ok := w.field(field); ok {
|
|
2643
|
+
return probeRowVal(v)
|
|
2644
|
+
}
|
|
2645
|
+
return RowProbe{Kind: probeAbsent}
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
func (w WireRow) ProbeList(field string) ListProbe {
|
|
2649
|
+
if v, ok := w.field(field); ok {
|
|
2650
|
+
return probeListVal(v)
|
|
2651
|
+
}
|
|
2652
|
+
return ListProbe{Kind: probeAbsent}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
type WireList struct {
|
|
2656
|
+
items []${PKG}.Value
|
|
2657
|
+
}
|
|
2658
|
+
|
|
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]) }
|
|
2665
|
+
|
|
2666
|
+
// FailureDetail exposes the structured Error Value fields WITHOUT naming the local ErrorDetail type, so
|
|
2667
|
+
// the harness can assert the de-box's kind / field / expectedType / actualWireType / rawValue (TEST
|
|
2668
|
+
// glue — same package as the covered module, so it may read the fields directly).
|
|
2669
|
+
func (e *BehaviorError) FailureDetail() (kind, model, field, expectedType, actualWireType, rawValue string, ok bool) {
|
|
2670
|
+
if e.Detail == nil {
|
|
2671
|
+
return "", "", "", "", "", "", false
|
|
2672
|
+
}
|
|
2673
|
+
d := e.Detail
|
|
2674
|
+
return d.Kind, d.Model, d.Field, d.ExpectedType, d.ActualWireType, d.RawValue, true
|
|
2675
|
+
}`
|
|
2676
|
+
: "";
|
|
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). ──
|
|
2225
2687
|
const methodImpls = [];
|
|
2226
|
-
const emittedDecode = new Set();
|
|
2227
2688
|
for (const c of native) {
|
|
2228
2689
|
const typedNodes = new Map();
|
|
2229
2690
|
for (const n of c.body)
|
|
@@ -2234,113 +2695,117 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
|
2234
2695
|
continue; // #108: cond has no handler method (pure Expression) — no scripted impl.
|
|
2235
2696
|
const ref = typedNodes.get(n.id);
|
|
2236
2697
|
if ("fanout" in n) {
|
|
2237
|
-
//
|
|
2238
|
-
//
|
|
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).
|
|
2239
2700
|
const f = n.fanout;
|
|
2240
|
-
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
2241
|
-
const elemT = rawElemStructName(c.name, n.id);
|
|
2242
|
-
const batchT = rawRowStructName(c.name, n.id);
|
|
2243
|
-
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2244
2701
|
const portsT = `${portsStructName(c.name, n.id)}_batch`;
|
|
2245
2702
|
const foBoundT = boundGoType(n, c, typedNodes, plan);
|
|
2246
|
-
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${foBoundT}) (
|
|
2703
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${foBoundT}) ([]*WireValue, error) {
|
|
2247
2704
|
src, ok := s.next(${JSON.stringify(f.component)})
|
|
2248
2705
|
if !ok {
|
|
2249
|
-
return ${
|
|
2706
|
+
return nil, unknownComponent(${JSON.stringify(f.component)})
|
|
2250
2707
|
}
|
|
2251
2708
|
if src.isErr {
|
|
2252
|
-
return
|
|
2709
|
+
return nil, leafFailure(src.err)
|
|
2253
2710
|
}
|
|
2254
2711
|
arr, _ := src.ok.([]${PKG}.Value)
|
|
2255
|
-
|
|
2712
|
+
wires := make([]*WireValue, 0, len(arr))
|
|
2256
2713
|
for _, ev := range arr {
|
|
2257
|
-
// v3: a null aligned body is a DANGLING id — decode it to the zero elem row (empty dedupeKey
|
|
2258
|
-
// field), so the runner's dangling-drop excludes it (byte-equal to fanoutDedupDrop).
|
|
2259
2714
|
if ev == nil {
|
|
2260
|
-
|
|
2715
|
+
wires = append(wires, nil)
|
|
2261
2716
|
continue
|
|
2262
2717
|
}
|
|
2263
|
-
|
|
2718
|
+
wv := WireValue{v: ev}
|
|
2719
|
+
wires = append(wires, &wv)
|
|
2264
2720
|
}
|
|
2265
|
-
return
|
|
2721
|
+
return wires, nil
|
|
2266
2722
|
}`);
|
|
2267
2723
|
continue;
|
|
2268
2724
|
}
|
|
2269
2725
|
if ("map" in n) {
|
|
2726
|
+
// map: batched → one WIRE per element ([]WireValue); non-batched → one element WIRE (WireValue).
|
|
2270
2727
|
const m = n.map;
|
|
2271
2728
|
const batched = m.batched === true;
|
|
2272
|
-
const arrRef = ref;
|
|
2273
|
-
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
2274
|
-
const elemT = rawElemStructName(c.name, n.id);
|
|
2275
|
-
const batchT = rawRowStructName(c.name, n.id);
|
|
2276
|
-
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2277
2729
|
const portsT = batched ? `${portsStructName(c.name, n.id)}_batch` : portsStructName(c.name, n.id);
|
|
2278
|
-
const retT = batched ? batchT : elemT;
|
|
2279
2730
|
const mapBoundT = boundGoType(n, c, typedNodes, plan);
|
|
2280
2731
|
if (batched) {
|
|
2281
|
-
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (
|
|
2732
|
+
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) ([]WireValue, error) {
|
|
2282
2733
|
src, ok := s.next(${JSON.stringify(m.component)})
|
|
2283
2734
|
if !ok {
|
|
2284
|
-
return ${
|
|
2735
|
+
return nil, unknownComponent(${JSON.stringify(m.component)})
|
|
2285
2736
|
}
|
|
2286
2737
|
if src.isErr {
|
|
2287
|
-
return
|
|
2738
|
+
return nil, leafFailure(src.err)
|
|
2288
2739
|
}
|
|
2289
2740
|
arr, _ := src.ok.([]${PKG}.Value)
|
|
2290
|
-
|
|
2741
|
+
wires := make([]WireValue, 0, len(arr))
|
|
2291
2742
|
for _, ev := range arr {
|
|
2292
|
-
|
|
2743
|
+
wires = append(wires, WireValue{v: ev})
|
|
2293
2744
|
}
|
|
2294
|
-
return
|
|
2745
|
+
return wires, nil
|
|
2295
2746
|
}`);
|
|
2296
2747
|
}
|
|
2297
2748
|
else {
|
|
2298
|
-
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) {
|
|
2299
2750
|
src, ok := s.next(${JSON.stringify(m.component)})
|
|
2300
2751
|
if !ok {
|
|
2301
|
-
return
|
|
2752
|
+
return WireValue{}, unknownComponent(${JSON.stringify(m.component)})
|
|
2302
2753
|
}
|
|
2303
2754
|
if src.isErr {
|
|
2304
|
-
return
|
|
2755
|
+
return WireValue{}, leafFailure(src.err)
|
|
2305
2756
|
}
|
|
2306
|
-
return
|
|
2757
|
+
return WireValue{v: src.ok}, nil
|
|
2307
2758
|
}`);
|
|
2308
2759
|
}
|
|
2309
|
-
void retT;
|
|
2310
2760
|
continue;
|
|
2311
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.
|
|
2312
2764
|
const node = n;
|
|
2313
|
-
const rowT = rawRowStructName(c.name, node.id);
|
|
2314
|
-
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
2315
2765
|
const nodeBoundT = boundGoType(node, c, typedNodes, plan);
|
|
2316
|
-
// #129
|
|
2317
|
-
//
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
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}`);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
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;
|
|
2798
|
+
const portsParam = canEcho ? "ports" : "_";
|
|
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) {
|
|
2336
2801
|
src, ok := s.next(${JSON.stringify(node.component)})
|
|
2337
2802
|
if !ok {
|
|
2338
|
-
return
|
|
2803
|
+
return WireValue{}, unknownComponent(${JSON.stringify(node.component)})
|
|
2339
2804
|
}
|
|
2340
2805
|
if src.isErr {
|
|
2341
|
-
return
|
|
2806
|
+
return WireValue{}, leafFailure(src.err)
|
|
2342
2807
|
}
|
|
2343
|
-
${
|
|
2808
|
+
${echoBlock} return WireValue{v: src.ok}, nil
|
|
2344
2809
|
}`);
|
|
2345
2810
|
}
|
|
2346
2811
|
}
|
|
@@ -2355,10 +2820,10 @@ type scriptSrc struct {
|
|
|
2355
2820
|
ok ${PKG}.Value
|
|
2356
2821
|
}
|
|
2357
2822
|
|
|
2358
|
-
// ScriptedNativeRaw — the CONCRETE test handler:
|
|
2359
|
-
//
|
|
2360
|
-
//
|
|
2361
|
-
// 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.
|
|
2362
2827
|
type ScriptedNativeRaw struct {
|
|
2363
2828
|
mu sync.Mutex
|
|
2364
2829
|
queues map[string][]scriptSrc
|
|
@@ -2383,15 +2848,13 @@ func NewScriptedNativeRaw(spec *${PKG}.JObj) (*ScriptedNativeRaw, error) {
|
|
|
2383
2848
|
for _, e := range entries {
|
|
2384
2849
|
eo, _ := e.(*${PKG}.JObj)
|
|
2385
2850
|
// #129: an \`echo\` directive names an input port to echo back (mirrors the reference
|
|
2386
|
-
// scriptedHandlers'
|
|
2387
|
-
// payload. \`{"echo":"port","port":"X"}\` is normalized to the port NAME here, so the
|
|
2388
|
-
// generated node method just compares \`src.echo\` against its echoable port names —
|
|
2389
|
-
// \`{"echo":"items"}\` is already that shape.
|
|
2851
|
+
// scriptedHandlers' \`echo === "items"\`). It is drained together with the ok payload.
|
|
2390
2852
|
echo := ""
|
|
2391
2853
|
if echoNode, has := eo.Get("echo"); has {
|
|
2392
2854
|
echo, _ = echoNode.(string)
|
|
2855
|
+
// normalize echo:port to the port NAME X so a de-box handler's echo comparison fires;
|
|
2856
|
+
// echo:items already names the port directly.
|
|
2393
2857
|
if echo == "port" {
|
|
2394
|
-
echo = ""
|
|
2395
2858
|
if portNode, hasPort := eo.Get("port"); hasPort {
|
|
2396
2859
|
echo, _ = portNode.(string)
|
|
2397
2860
|
}
|
|
@@ -2434,86 +2897,52 @@ func (s *ScriptedNativeRaw) next(component string) (scriptSrc, bool) {
|
|
|
2434
2897
|
}`;
|
|
2435
2898
|
return `// GENERATED test-only observe companion (bc#77) — NOT part of the covered module. Serializes the
|
|
2436
2899
|
// struct-native runner's result to a canonical Value for the equivalence pin, and carries the CONCRETE
|
|
2437
|
-
// scripted test handler (
|
|
2438
|
-
//
|
|
2439
|
-
// native +
|
|
2440
|
-
// 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.
|
|
2441
2904
|
package behaviors
|
|
2442
2905
|
|
|
2443
2906
|
import (
|
|
2444
|
-
"sync"
|
|
2907
|
+
${anyDeBox ? '\t"strconv"\n\n' : ""} "sync"
|
|
2445
2908
|
|
|
2446
2909
|
${PKG} "${runtimeImport ?? "github.com/foo-ogawa/behavior-contracts/go"}"
|
|
2447
2910
|
)
|
|
2448
2911
|
|
|
2912
|
+
// scripted wire adapter (TEST glue) — the CONCRETE WireValue/WireRow/WireList the covered module references.
|
|
2913
|
+
${wireAdapter}
|
|
2914
|
+
|
|
2449
2915
|
// typed -> Value serializers (TEST glue only — the covered module has none).
|
|
2450
2916
|
${serializers}
|
|
2451
2917
|
|
|
2452
|
-
// Value -> typed struct marshallers (+ must* helpers) for the
|
|
2918
|
+
// Value -> typed struct marshallers (+ must* helpers) for the input decoders (TEST glue only).
|
|
2453
2919
|
${marshallers}
|
|
2454
2920
|
|
|
2455
2921
|
${mustHelpers}
|
|
2456
2922
|
|
|
2457
2923
|
${arrOptHelpers}
|
|
2458
2924
|
|
|
2459
|
-
// per-node concrete-row decoders (scripted Value -> RawRow_/RawElem_ struct).
|
|
2460
|
-
${decodeFns.join("\n\n")}
|
|
2461
|
-
|
|
2462
2925
|
// input decoders (generic *Obj -> concrete In_<comp>; TEST glue, one decode at the observe boundary).
|
|
2463
2926
|
${inDecoders}
|
|
2464
2927
|
|
|
2465
2928
|
${adapter}
|
|
2466
2929
|
|
|
2467
|
-
|
|
2930
|
+
// per-component CONCRETE handler wrappers (the runner takes one by value — embeds the shared queue).
|
|
2931
|
+
${handlerWrappers}
|
|
2468
2932
|
|
|
2469
|
-
//
|
|
2933
|
+
// ScriptedNativeRaw method impls (one per covered node — the concrete Node_* seam returning WireValue).
|
|
2470
2934
|
${methodImpls.join("\n\n")}
|
|
2471
2935
|
|
|
2472
|
-
// ObserveNativeRaw drives the struct-native runner and serializes to a canonical Value (test only).
|
|
2473
|
-
//
|
|
2474
|
-
//
|
|
2475
|
-
//
|
|
2476
|
-
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) {
|
|
2477
2941
|
${arms}
|
|
2478
2942
|
return nil, ${PKG}.NewBehaviorFailure("UNKNOWN_ENTRY", "component '"+name+"' is not a covered read (fail-closed)")
|
|
2479
2943
|
}
|
|
2480
2944
|
`;
|
|
2481
2945
|
}
|
|
2482
|
-
/** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
|
|
2483
|
-
* declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row (a scalar/arr/
|
|
2484
|
-
* opt row via its Val; a NAMED row via its flattened fields copied from the struct port). A port with a
|
|
2485
|
-
* different type is not echoable (the row could not hold it). */
|
|
2486
|
-
function echoablePorts(comp, node, ref, plan, typedNodes) {
|
|
2487
|
-
const want = renderTypeRef(ref);
|
|
2488
|
-
return Object.keys(node.ports).filter((p) => portFieldGoType(node.ports[p], comp, plan, typedNodes, `port '${p}'`) === want);
|
|
2489
|
-
}
|
|
2490
|
-
/** emitDecodeRow — emit (once, deduped) a decoder `decodeRow_<comp>_<node><suffix>(v Value) <rowT>`
|
|
2491
|
-
* that materializes the scripted ok Value into the concrete row struct (typed fields = the outType /
|
|
2492
|
-
* elem-row type). Named: reuse the Value->struct marshaller + copy fields; scalar/arr/opt: materialize
|
|
2493
|
-
* into .Val. TEST glue only (the production consumer decodes its own wire payload). */
|
|
2494
|
-
function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix) {
|
|
2495
|
-
const fn = `decodeRow_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}${suffix}`;
|
|
2496
|
-
if (emitted.has(fn))
|
|
2497
|
-
return fn;
|
|
2498
|
-
emitted.add(fn);
|
|
2499
|
-
const lines = [];
|
|
2500
|
-
lines.push(`func ${fn}(v ${PKG}.Value) ${rowT} {`);
|
|
2501
|
-
lines.push(`\tvar out ${rowT}`);
|
|
2502
|
-
if (ref.kind === "named") {
|
|
2503
|
-
const decl = findDecl(plan, ref.name);
|
|
2504
|
-
lines.push(`\tt := ${goTypedInternals.materializeExpr("v", ref, plan)}`);
|
|
2505
|
-
for (const f of decl.fields) {
|
|
2506
|
-
lines.push(`\tout.${goFieldName(f.name)} = t.${goFieldName(f.name)}`);
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
else {
|
|
2510
|
-
lines.push(`\tout.Val = ${goTypedInternals.materializeExpr("v", ref, plan)}`);
|
|
2511
|
-
}
|
|
2512
|
-
lines.push(`\treturn out`);
|
|
2513
|
-
lines.push(`}`);
|
|
2514
|
-
out.push(lines.join("\n"));
|
|
2515
|
-
return fn;
|
|
2516
|
-
}
|
|
2517
2946
|
/**
|
|
2518
2947
|
* goTypedNativeEmitter — language id `go-typed-native` (bc#77). Emits the generic straight-line
|
|
2519
2948
|
* body (for non-covered components / helpers) plus the COMBINED READ layer (native ports struct
|