behavior-contracts 0.8.2 → 0.8.4
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 +1 -1
- package/dist/authoring.d.ts.map +1 -1
- package/dist/authoring.js +41 -2
- package/dist/authoring.js.map +1 -1
- package/dist/behavior.d.ts +0 -24
- package/dist/behavior.d.ts.map +1 -1
- package/dist/behavior.js +24 -2
- package/dist/behavior.js.map +1 -1
- package/dist/generator/emit-shared-go-typed.d.ts.map +1 -1
- package/dist/generator/emit-shared-go-typed.js +7 -7
- package/dist/generator/emit-shared-go-typed.js.map +1 -1
- package/dist/generator/emit-shared-rust-typed.d.ts +8 -0
- package/dist/generator/emit-shared-rust-typed.d.ts.map +1 -1
- package/dist/generator/emit-shared-rust-typed.js +20 -6
- package/dist/generator/emit-shared-rust-typed.js.map +1 -1
- package/dist/generator/emit-shared-typescript.d.ts.map +1 -1
- package/dist/generator/emit-shared-typescript.js +18 -2
- package/dist/generator/emit-shared-typescript.js.map +1 -1
- package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-go.js +185 -85
- package/dist/generator/emit-typed-native-go.js.map +1 -1
- package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-rust.js +231 -101
- package/dist/generator/emit-typed-native-rust.js.map +1 -1
- package/dist/generator/native-expr.d.ts +68 -5
- package/dist/generator/native-expr.d.ts.map +1 -1
- package/dist/generator/native-expr.js +94 -18
- package/dist/generator/native-expr.js.map +1 -1
- package/dist/generator/typed.d.ts +26 -0
- package/dist/generator/typed.d.ts.map +1 -1
- package/dist/generator/typed.js +52 -0
- package/dist/generator/typed.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@ import { GeneratorFailure } from "./core.js";
|
|
|
2
2
|
import { isControlGate } from "../behavior.js";
|
|
3
3
|
import { goStraightlineInternals } from "./emit-shared-go.js";
|
|
4
4
|
import { goTypedInternals } from "./emit-shared-go-typed.js";
|
|
5
|
-
import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
|
|
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
8
|
const PKG = "dslcontracts";
|
|
@@ -72,18 +72,13 @@ function inferPortType(node, inputPorts, asBinding) {
|
|
|
72
72
|
}
|
|
73
73
|
return "Value";
|
|
74
74
|
}
|
|
75
|
+
// a ref to a REQUIRED scalar input port → that scalar. An OPTIONAL port has no required-scalar
|
|
76
|
+
// kind (its native type is *T) and is lowered by the opt lane ahead of this — reaching here with
|
|
77
|
+
// one means the shape is uncovered, and "Value" routes it to the caller's loud fail-closed.
|
|
75
78
|
if (e.path.length === 1) {
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
return "string"; // literal = constrained string.
|
|
80
|
-
if (s.type === "int")
|
|
81
|
-
return "int64";
|
|
82
|
-
if (s.type === "float" || s.type === "number")
|
|
83
|
-
return "float64"; // `"number"` = the float channel.
|
|
84
|
-
if (s.type === "bool")
|
|
85
|
-
return "bool";
|
|
86
|
-
}
|
|
79
|
+
const sc = inputScalarKind(inputPorts[e.path[0]]);
|
|
80
|
+
if (sc !== undefined)
|
|
81
|
+
return sc;
|
|
87
82
|
}
|
|
88
83
|
return "Value";
|
|
89
84
|
}
|
|
@@ -121,6 +116,12 @@ function staticStringArrElems(node) {
|
|
|
121
116
|
* `dslcontracts.Value` fallback on the native module). `where` names the emission site for the error.
|
|
122
117
|
*/
|
|
123
118
|
function portFieldGoType(node, inputPorts, where = "port", asBinding, plan, typedNodes) {
|
|
119
|
+
// A port reading an OPTIONAL input port lowers through the shared native-expr compiler (the *T
|
|
120
|
+
// pass-through / the coalesce default). It runs FIRST so an optional input can never fall into the
|
|
121
|
+
// required-scalar inference below (which has no way to say "absent").
|
|
122
|
+
const optPort = optPortCompileG(node, inputPorts, where, plan);
|
|
123
|
+
if (optPort !== null)
|
|
124
|
+
return renderTypeRef(optPort.ref);
|
|
124
125
|
if (staticStringArrElems(node) !== null)
|
|
125
126
|
return "[]string";
|
|
126
127
|
// #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → []ElemT.
|
|
@@ -141,7 +142,7 @@ function portFieldGoType(node, inputPorts, where = "port", asBinding, plan, type
|
|
|
141
142
|
return renderTypeRef(composite);
|
|
142
143
|
const ft = inferPortType(node, inputPorts, asBinding);
|
|
143
144
|
if (ft === "Value") {
|
|
144
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): ${where} '${JSON.stringify(node)}' does not lower to a native Go type (would need a boxed dslcontracts.Value). The covered read plane is runtime-free and admits NO boxed escape
|
|
145
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): ${where} '${JSON.stringify(node)}' does not lower to a native Go type (would need a boxed dslcontracts.Value). The covered read plane is runtime-free and admits NO boxed escape. ${GO_STATIC_PORT_FORMS} Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
|
|
145
146
|
}
|
|
146
147
|
return ft;
|
|
147
148
|
}
|
|
@@ -279,14 +280,20 @@ function numLiteralGoExpr(node) {
|
|
|
279
280
|
// fractional / exponent literal → checked float64 (finite, already guaranteed above).
|
|
280
281
|
return `${PKG}.Value(float64(${node}))`;
|
|
281
282
|
}
|
|
282
|
-
/** A port node is static (literal / ref / concat of those / static arr / bare number literal
|
|
283
|
-
* resolvable to a native Go
|
|
284
|
-
|
|
283
|
+
/** A port node is static (literal / ref / concat of those / static arr / bare number literal / an
|
|
284
|
+
* optional-input read) — resolvable to a native Go value with no NewObj / no evaluate interpreter. This
|
|
285
|
+
* is the SHAPE gate that decides native ELIGIBILITY; the lowering itself (portFieldGoType / emitPortInit)
|
|
286
|
+
* is what proves each shape, and fails closed loudly on one it cannot lower. */
|
|
287
|
+
function portIsStatic(node, inputPorts) {
|
|
285
288
|
const arr = staticArrElems(node);
|
|
286
289
|
if (arr !== null)
|
|
287
|
-
return arr.every(portIsStatic);
|
|
290
|
+
return arr.every((el) => portIsStatic(el, inputPorts));
|
|
288
291
|
if (numLiteralGoExpr(node) !== null)
|
|
289
292
|
return true; // bare number literal (e.g. Query `limit`)
|
|
293
|
+
// An optional-input read (`opt($.x)` / `coalesce(opt($.x), 20)` / `ne(opt($.x), null)`) is a
|
|
294
|
+
// statically-resolvable port — the opt lane lowers it (or fails closed with the specific reason).
|
|
295
|
+
if (inputPorts !== undefined && exprReadsOptionalInput(node, inputPorts))
|
|
296
|
+
return true;
|
|
290
297
|
const e = classifyExpr(node);
|
|
291
298
|
switch (e.kind) {
|
|
292
299
|
case "str":
|
|
@@ -295,7 +302,7 @@ function portIsStatic(node) {
|
|
|
295
302
|
case "ref":
|
|
296
303
|
return true;
|
|
297
304
|
case "concat":
|
|
298
|
-
return e.parts.every((p) => portIsStatic(reconstructG(p)));
|
|
305
|
+
return e.parts.every((p) => portIsStatic(reconstructG(p), inputPorts));
|
|
299
306
|
default:
|
|
300
307
|
return false;
|
|
301
308
|
}
|
|
@@ -399,7 +406,7 @@ function coveredMapNode(n, bodyIds, comp) {
|
|
|
399
406
|
}
|
|
400
407
|
const ports = m.ports;
|
|
401
408
|
for (const p of Object.keys(ports))
|
|
402
|
-
if (!portIsStatic(ports[p]))
|
|
409
|
+
if (!portIsStatic(ports[p], comp.inputPorts))
|
|
403
410
|
return false;
|
|
404
411
|
return true;
|
|
405
412
|
}
|
|
@@ -432,7 +439,7 @@ function coveredFanoutNode(n, bodyIds, comp) {
|
|
|
432
439
|
}
|
|
433
440
|
const ports = f.ports;
|
|
434
441
|
for (const p of Object.keys(ports))
|
|
435
|
-
if (!portIsStatic(ports[p]))
|
|
442
|
+
if (!portIsStatic(ports[p], comp.inputPorts))
|
|
436
443
|
return false;
|
|
437
444
|
return true;
|
|
438
445
|
}
|
|
@@ -539,7 +546,7 @@ function coverageRejectReason(comp) {
|
|
|
539
546
|
if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
540
547
|
return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
|
|
541
548
|
for (const p of Object.keys(cr.ports))
|
|
542
|
-
if (portIsStatic(cr.ports[p]) === false)
|
|
549
|
+
if (portIsStatic(cr.ports[p], comp.inputPorts) === false)
|
|
543
550
|
return `node '${n.id}' port '${p}' is not statically resolvable`;
|
|
544
551
|
}
|
|
545
552
|
if (!outputIsNativeLowerable(comp))
|
|
@@ -1030,43 +1037,31 @@ function reconstructG(e) {
|
|
|
1030
1037
|
function inStructName(compName) {
|
|
1031
1038
|
return `In_${sanitize(compName)}`;
|
|
1032
1039
|
}
|
|
1033
|
-
/**
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
return `${PKG}.Value`;
|
|
1056
|
-
}
|
|
1057
|
-
case "map": {
|
|
1058
|
-
// an input MAP port (dynamic string keys, declared value elemType) lowers to a native map[string]ElemT.
|
|
1059
|
-
const et = schema.elemType;
|
|
1060
|
-
if (et !== undefined && plan !== undefined)
|
|
1061
|
-
return `map[string]${renderTypeRef(deriveTypeRef(et, plan))}`;
|
|
1062
|
-
return `${PKG}.Value`;
|
|
1063
|
-
}
|
|
1064
|
-
default:
|
|
1065
|
-
return `${PKG}.Value`;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
/** The scalar kind an input port declares, or undefined if it is not a native scalar. */
|
|
1040
|
+
/**
|
|
1041
|
+
* inputPortGoType — the native Go type for an input port's declared type. Scalars lower to the concrete
|
|
1042
|
+
* scalar; `array`/`map` with a declared elemType to `[]ElemT` / `map[string]ElemT`; an OPTIONAL port
|
|
1043
|
+
* (`{opt:T}` → `required:false`) to `*T` — the native representation of "the value is absent" (nil).
|
|
1044
|
+
* A declared type with no native lowering stays the boxed Value.
|
|
1045
|
+
*
|
|
1046
|
+
* An OPTIONAL port whose inner type has no native lowering FAILS CLOSED: a boxed `Value` cannot carry
|
|
1047
|
+
* the presence distinction on the covered plane, and emitting the bare inner type would silently turn
|
|
1048
|
+
* "absent" into a zero value. A port whose optionality cannot be represented is never silently coerced.
|
|
1049
|
+
*/
|
|
1050
|
+
function inputPortGoType(schema, plan, where = "input port") {
|
|
1051
|
+
const ref = inputPortTypeRef(schema, plan);
|
|
1052
|
+
if (ref !== undefined)
|
|
1053
|
+
return renderTypeRef(ref);
|
|
1054
|
+
if (inputPortIsOptional(schema))
|
|
1055
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Go lowering, so "absent" has no native representation (a boxed Value would be an escape, and the bare inner type would silently read absent as a zero value). Declare an elemType for an array/map, or use a natively-lowerable scalar.`);
|
|
1056
|
+
return `${PKG}.Value`;
|
|
1057
|
+
}
|
|
1058
|
+
/** The scalar kind a REQUIRED input port declares, or undefined when it is not a required native scalar.
|
|
1059
|
+
* An OPTIONAL port is deliberately NOT a scalar kind here: its native type is `*T`, and the callers of
|
|
1060
|
+
* this function build REQUIRED scalar slots (`in.<Field>` read straight into a non-opt port field).
|
|
1061
|
+
* Answering "int64" for an `{opt:"int"}` port is exactly how "absent" would silently become `0`. */
|
|
1069
1062
|
function inputScalarKind(schema) {
|
|
1063
|
+
if (inputPortIsOptional(schema))
|
|
1064
|
+
return undefined;
|
|
1070
1065
|
switch (schema?.type) {
|
|
1071
1066
|
case "string":
|
|
1072
1067
|
case "literal": // a `"literal"` union is a constrained string (see inputPortGoType).
|
|
@@ -1091,7 +1086,7 @@ function emitInStruct(comp, plan) {
|
|
|
1091
1086
|
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
|
|
1092
1087
|
}
|
|
1093
1088
|
const names = ports.map((p) => goFieldName(p));
|
|
1094
|
-
const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan));
|
|
1089
|
+
const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`));
|
|
1095
1090
|
const nameW = Math.max(0, ...names.map((n) => n.length));
|
|
1096
1091
|
const typeW = Math.max(0, ...types.map((t) => t.length));
|
|
1097
1092
|
const fields = ports
|
|
@@ -1132,6 +1127,15 @@ function emitInDecoder(comp, plan) {
|
|
|
1132
1127
|
const scalar = inputScalarKind(schema);
|
|
1133
1128
|
const et = schema.elemType;
|
|
1134
1129
|
lines.push(`\tif v, ok := input.Get(${JSON.stringify(p)}); ok {`);
|
|
1130
|
+
if (inputPortIsOptional(schema)) {
|
|
1131
|
+
// An OPTIONAL port decodes into its native *T. materializeExpr's opt de-box IS the wire rule:
|
|
1132
|
+
// a nil Value (null) → nil (absent), any other value → a pointer to the materialized inner. A key
|
|
1133
|
+
// that never appears leaves the struct's zero value (nil) — the same absent value. Checked FIRST:
|
|
1134
|
+
// optionality wraps every inner type (an optional array is *[]T, not []T).
|
|
1135
|
+
lines.push(`\t\tin.${field} = ${goTypedInternals.materializeExpr("v", inputPortTypeRef(schema, plan), plan)}`);
|
|
1136
|
+
lines.push(`\t}`);
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1135
1139
|
if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
1136
1140
|
// #108: decode the boxed Value array into the native []ElemT (test glue — materialize each element
|
|
1137
1141
|
// via the typed materializer; a non-array or non-conforming element is a fail-closed decode error).
|
|
@@ -1174,6 +1178,9 @@ function emitInDecoder(comp, plan) {
|
|
|
1174
1178
|
lines.push(`}`);
|
|
1175
1179
|
return lines.join("\n");
|
|
1176
1180
|
}
|
|
1181
|
+
/** The port forms the STATIC (non-optional) lane lowers — named in its reject message so the message
|
|
1182
|
+
* describes what is actually covered rather than a stale corpus list. */
|
|
1183
|
+
const GO_STATIC_PORT_FORMS = "This port does not read an optional input, so it lowers through the STATIC port forms: a string / bool / number literal, a static string-array (projection), a ref to a REQUIRED scalar input port, a ref to an input array/map port or a prior node's array (declared elemType), a ref to a `$as` element field, or a concat of native strings. A dynamic operator at port position is not covered on this lane. (A port that DOES read an optional input lowers through the full native expression compiler, which admits the whole closed operator set — so the covered operator set at port position currently depends on the lane, not on the operator.)";
|
|
1177
1184
|
/**
|
|
1178
1185
|
* emitNativeScalar — lower a static port expr to a NATIVE Go expression of the concrete `want` scalar
|
|
1179
1186
|
* type, or null if it cannot be statically proven that type. Literals: a string literal is native for
|
|
@@ -1231,6 +1238,11 @@ function emitNativeScalar(node, want, resolveRef) {
|
|
|
1231
1238
|
*/
|
|
1232
1239
|
function emitPortInit(pn, expr, fieldGoType, resolveRef, inputPorts, plan, typedNodes, asBinding, elemBaseExpr) {
|
|
1233
1240
|
const fieldName = goPortFieldName(pn);
|
|
1241
|
+
// The OPTIONAL-input lane (*T pass-through / coalesce default) — same shared compile as
|
|
1242
|
+
// portFieldGoType, so the field type and its initializer are derived from ONE lowering.
|
|
1243
|
+
const optPort = optPortCompileG(expr, inputPorts ?? {}, `port '${pn}'`, plan);
|
|
1244
|
+
if (optPort !== null)
|
|
1245
|
+
return `${fieldName}: ${optPort.expr}`;
|
|
1234
1246
|
// #108-map: a port that is a ref to a `$as` element MAP/NAMED field (or an input map port) → the typed
|
|
1235
1247
|
// composite value directly off the element/input (native map[string]V / struct — ZERO boxed Value).
|
|
1236
1248
|
if (plan !== undefined) {
|
|
@@ -1283,7 +1295,7 @@ function emitPortInit(pn, expr, fieldGoType, resolveRef, inputPorts, plan, typed
|
|
|
1283
1295
|
// FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would
|
|
1284
1296
|
// re-couple the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the
|
|
1285
1297
|
// component is not native-eligible and must regenerate on the boxed straight-line path.
|
|
1286
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): port '${pn}' (${JSON.stringify(expr)}) does not lower to a native Go value of type '${fieldGoType}'. The covered read plane admits NO boxed dslcontracts.Value escape
|
|
1298
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): port '${pn}' (${JSON.stringify(expr)}) does not lower to a native Go value of type '${fieldGoType}'. The covered read plane admits NO boxed dslcontracts.Value escape. ${GO_STATIC_PORT_FORMS} Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
|
|
1287
1299
|
}
|
|
1288
1300
|
/**
|
|
1289
1301
|
* emitParallelStageArm (bc#87) — EXPLICIT static parallel orchestration for a real-concurrency stage.
|
|
@@ -2019,23 +2031,72 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
|
|
|
2019
2031
|
lines.push(c);
|
|
2020
2032
|
return lines.join("\n");
|
|
2021
2033
|
}
|
|
2022
|
-
/**
|
|
2023
|
-
*
|
|
2024
|
-
*
|
|
2034
|
+
/**
|
|
2035
|
+
* goInputHeadRef (#108) — resolve an INPUT port head for a native-expr ref (cond `if` / map `when` /
|
|
2036
|
+
* branches / an opt-lane port). The port's declared type comes from the shared `inputPortTypeRef` SSoT,
|
|
2037
|
+
* so an OPTIONAL port resolves as `{opt:…}` (native `*T`) and the compiler can see its optionality: a
|
|
2038
|
+
* null-compare becomes a real presence test and a coalesce a real deref-or-default, while a
|
|
2039
|
+
* REQUIRED-scalar slot fed by an opt fails closed. Go values are read directly off the input struct (no
|
|
2040
|
+
* ownership ceremony); a type with no native lowering → null (fail-closed).
|
|
2041
|
+
*/
|
|
2025
2042
|
function goInputHeadRef(head, comp, plan) {
|
|
2026
|
-
const
|
|
2027
|
-
if (
|
|
2043
|
+
const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
|
|
2044
|
+
if (ref === undefined)
|
|
2028
2045
|
return null;
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2046
|
+
return { expr: `in.${goFieldName(head)}`, ref };
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* optPortCompileG — the OPTIONAL-input port lane (go). The rust twin's mirror: a component may
|
|
2050
|
+
* declare an input port `{opt:T}` (PortSchema `required:false`), and the only native representation of
|
|
2051
|
+
* "the value is absent" is `*T`, so such a port never lowers through the required-scalar inference. The
|
|
2052
|
+
* port expression compiles with the SHARED runtime-free native-expr compiler (native-expr.ts — the same
|
|
2053
|
+
* traversal, type-inference and fail-closed discipline rust uses, so the two agree by construction):
|
|
2054
|
+
*
|
|
2055
|
+
* - `opt($.x)` / `$.x` at port position → `*T` (the leaf receives the absent value as null, exactly
|
|
2056
|
+
* as run_behavior's `evalPorts` passes it).
|
|
2057
|
+
* - `coalesce(opt($.x), <literal>)` → the native deref-or-default — a native `T`, no boxed Value.
|
|
2058
|
+
* - `ne(opt($.x), null)` / `eq(…, null)` → the native presence test (`!= nil` / `== nil`).
|
|
2059
|
+
*
|
|
2060
|
+
* Returns null when the port expression reads NO optional input (the caller keeps its existing lowering).
|
|
2061
|
+
* A port that DOES read one but does not compile — or that compiles to a fallible expression, which this
|
|
2062
|
+
* position cannot hoist — FAILS CLOSED loudly rather than degrade to a required type or a zero value.
|
|
2063
|
+
*/
|
|
2064
|
+
function optPortCompileG(node, inputPorts, where, plan) {
|
|
2065
|
+
if (plan === undefined)
|
|
2066
|
+
return null;
|
|
2067
|
+
if (!exprReadsOptionalInput(node, inputPorts))
|
|
2068
|
+
return null;
|
|
2069
|
+
const resolveHead = (head) => goInputHeadRef(head, { inputPorts }, plan);
|
|
2070
|
+
let c;
|
|
2071
|
+
try {
|
|
2072
|
+
// zeroOut is unreachable here: a port expression that needs an error return is rejected below
|
|
2073
|
+
// (stmts.length > 0), so the backend never renders an early-return with it.
|
|
2074
|
+
c = new NativeExprCompiler(makeGoExprBackend("", resolveHead, plan)).compilePort(node);
|
|
2075
|
+
}
|
|
2076
|
+
catch (e) {
|
|
2077
|
+
if (!(e instanceof GeneratorFailure))
|
|
2078
|
+
throw e;
|
|
2079
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} ${JSON.stringify(node)} reads an OPTIONAL input port but does not lower to a native Go value: ${e.message}`);
|
|
2080
|
+
}
|
|
2081
|
+
if (c.stmts.length > 0)
|
|
2082
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} ${JSON.stringify(node)} reads an OPTIONAL input port through a FALLIBLE expression (one that can raise INT_OVERFLOW / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS). Such an expression lowers to STATEMENTS carrying an early error-return, and a port is built inline in the ports-struct literal — a position that cannot host them. (A cond/guard position CAN host them, so the same expression is covered there — the boundary is the POSITION, not the operator.) Use a non-fallible port expression.`);
|
|
2083
|
+
return c;
|
|
2084
|
+
}
|
|
2085
|
+
/** exprReadsOptionalInput — does this port expression reference an OPTIONAL (`required:false`) input
|
|
2086
|
+
* port? Language-neutral scan for `ref`/`refOpt` heads bound to an optional port. Decides ONLY which
|
|
2087
|
+
* lane a port takes; the lane itself then proves the lowering (or fails closed). */
|
|
2088
|
+
function exprReadsOptionalInput(node, inputPorts) {
|
|
2089
|
+
if (node === null || typeof node !== "object")
|
|
2090
|
+
return false;
|
|
2091
|
+
if (Array.isArray(node))
|
|
2092
|
+
return node.some((el) => exprReadsOptionalInput(el, inputPorts));
|
|
2093
|
+
const rec = node;
|
|
2094
|
+
const keys = Object.keys(rec);
|
|
2095
|
+
if (keys.length === 1 && (keys[0] === "ref" || keys[0] === "refOpt")) {
|
|
2096
|
+
const path = rec[keys[0]];
|
|
2097
|
+
return Array.isArray(path) && typeof path[0] === "string" && inputPortIsOptional(inputPorts?.[path[0]]);
|
|
2037
2098
|
}
|
|
2038
|
-
return
|
|
2099
|
+
return keys.some((k) => exprReadsOptionalInput(rec[k], inputPorts));
|
|
2039
2100
|
}
|
|
2040
2101
|
/**
|
|
2041
2102
|
* emitCondArm — the struct-native exec of a covered cond node (#108). Mirrors run_behavior's cond
|
|
@@ -2281,6 +2342,23 @@ function makeGoExprBackend(zeroOut, resolveHead, plan) {
|
|
|
2281
2342
|
// opt (`*T`) null-presence test: present = `(expr != nil)`, absent = `(expr == nil)`.
|
|
2282
2343
|
optIsSome: (expr) => `(${expr} != nil)`,
|
|
2283
2344
|
optIsNone: (expr) => `(${expr} == nil)`,
|
|
2345
|
+
// opt (`*T`) defaulted to a PURE default. Go has no `unwrap_or`, so this is the same immediately-
|
|
2346
|
+
// invoked func literal the `ternary` rendering uses (the established shape in this emitter): bind the
|
|
2347
|
+
// pointer ONCE, deref it when present, else yield the default. The default sits in the else branch,
|
|
2348
|
+
// so it is evaluated only when absent — run_behavior's lazy right side exactly.
|
|
2349
|
+
optUnwrapOr: (optExpr, defaultExpr, innerTy) => `func() ${innerTy} { if v := ${optExpr}; v != nil { return *v }; return ${defaultExpr} }()`,
|
|
2350
|
+
// opt defaulted to a FALLIBLE default — a statement if/else, NOT a func literal: the default's
|
|
2351
|
+
// hoisted `return zeroOut, err` early returns must leave the ENCLOSING fn, which a func literal
|
|
2352
|
+
// would swallow (it would return from the literal). Only the nil branch evaluates the default.
|
|
2353
|
+
optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
|
|
2354
|
+
`var ${temp} ${ty}`,
|
|
2355
|
+
`if ${bindTemp} := ${optExpr}; ${bindTemp} != nil {`,
|
|
2356
|
+
` ${temp} = *${bindTemp}`,
|
|
2357
|
+
`} else {`,
|
|
2358
|
+
...dStmts.map((x) => ` ${x}`),
|
|
2359
|
+
` ${temp} = ${dExpr}`,
|
|
2360
|
+
`}`,
|
|
2361
|
+
],
|
|
2284
2362
|
ternary: (cond, t, e, ty) => `func() ${ty} { if ${cond} { return ${t} }; return ${e} }()`,
|
|
2285
2363
|
shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
|
|
2286
2364
|
// and: temp=false; if left { <rStmts>; temp=rExpr }. or: temp=true; if !left { <rStmts>; temp=rExpr }.
|
|
@@ -2713,17 +2791,20 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
|
|
|
2713
2791
|
const rowT = rawRowStructName(c.name, node.id);
|
|
2714
2792
|
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
2715
2793
|
const nodeBoundT = boundGoType(node, c, typedNodes, plan);
|
|
2716
|
-
// #129 (TEST glue):
|
|
2717
|
-
//
|
|
2718
|
-
//
|
|
2719
|
-
//
|
|
2720
|
-
//
|
|
2721
|
-
//
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2794
|
+
// #129 (TEST glue): support the reference scriptedHandlers' echo forms (PROTOCOL.md §3.5) — the
|
|
2795
|
+
// adapter normalizes `{"echo":"port","port":"X"}` / `{"echo":"items"}` to the port NAME, and the
|
|
2796
|
+
// scripted handler
|
|
2797
|
+
// ECHOES a NATIVE port back as its result. This makes a port-plane pin non-vacuous: the node's
|
|
2798
|
+
// observed result IS the native value that flowed into its ports struct, so a dropped / re-boxed /
|
|
2799
|
+
// zero-defaulted port diverges from run_behavior instead of passing. A port is echoable when its
|
|
2800
|
+
// native type IS the node's outType (that is exactly when the row's Val can hold it); `ports` is
|
|
2801
|
+
// named only when at least one port qualifies (else the param stays `_` so a non-echo node
|
|
2802
|
+
// compiles clean / unused-param-free).
|
|
2803
|
+
const echoable = echoablePorts(c, node, ref, plan, typedNodes);
|
|
2804
|
+
const portsParam = echoable.length > 0 ? "ports" : "_";
|
|
2805
|
+
const echoBranch = echoable
|
|
2806
|
+
.map((ep) => `\tif src.echo == ${JSON.stringify(ep)} {\n\t\treturn ${rowT}{Val: ports.${goPortFieldName(ep)}}, true\n\t}\n`)
|
|
2807
|
+
.join("");
|
|
2727
2808
|
methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, node.id)}(${portsParam} ${portsStructName(c.name, node.id)}, _ ${nodeBoundT}) (${rowT}, bool) {
|
|
2728
2809
|
src, ok := s.next(${JSON.stringify(node.component)})
|
|
2729
2810
|
if !ok {
|
|
@@ -2775,10 +2856,19 @@ func NewScriptedNativeRaw(spec *${PKG}.JObj) (*ScriptedNativeRaw, error) {
|
|
|
2775
2856
|
for _, e := range entries {
|
|
2776
2857
|
eo, _ := e.(*${PKG}.JObj)
|
|
2777
2858
|
// #129: an \`echo\` directive names an input port to echo back (mirrors the reference
|
|
2778
|
-
// scriptedHandlers'
|
|
2859
|
+
// scriptedHandlers' PROTOCOL.md §3.5 echo forms). It is drained together with the ok
|
|
2860
|
+
// payload. \`{"echo":"port","port":"X"}\` is normalized to the port NAME here, so the
|
|
2861
|
+
// generated node method just compares \`src.echo\` against its echoable port names —
|
|
2862
|
+
// \`{"echo":"items"}\` is already that shape.
|
|
2779
2863
|
echo := ""
|
|
2780
2864
|
if echoNode, has := eo.Get("echo"); has {
|
|
2781
2865
|
echo, _ = echoNode.(string)
|
|
2866
|
+
if echo == "port" {
|
|
2867
|
+
echo = ""
|
|
2868
|
+
if portNode, hasPort := eo.Get("port"); hasPort {
|
|
2869
|
+
echo, _ = portNode.(string)
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2782
2872
|
}
|
|
2783
2873
|
if okNode, has := eo.Get("ok"); has {
|
|
2784
2874
|
val, derr := ${PKG}.DecodeValue(okNode)
|
|
@@ -2862,6 +2952,16 @@ ${arms}
|
|
|
2862
2952
|
}
|
|
2863
2953
|
`;
|
|
2864
2954
|
}
|
|
2955
|
+
/** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
|
|
2956
|
+
* declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row's Val. A port
|
|
2957
|
+
* with a different type is not echoable (the row could not hold it), and a NAMED outType has no Val at
|
|
2958
|
+
* all (its row carries the struct's fields flattened — see emitDecodeRow). */
|
|
2959
|
+
function echoablePorts(comp, node, ref, plan, typedNodes) {
|
|
2960
|
+
if (ref.kind === "named")
|
|
2961
|
+
return [];
|
|
2962
|
+
const want = renderTypeRef(ref);
|
|
2963
|
+
return Object.keys(node.ports).filter((p) => portFieldGoType(node.ports[p], comp.inputPorts, `port '${p}'`, undefined, plan, typedNodes) === want);
|
|
2964
|
+
}
|
|
2865
2965
|
/** emitDecodeRow — emit (once, deduped) a decoder `decodeRow_<comp>_<node><suffix>(v Value) <rowT>`
|
|
2866
2966
|
* that materializes the scripted ok Value into the concrete row struct (typed fields = the outType /
|
|
2867
2967
|
* elem-row type). Named: reuse the Value->struct marshaller + copy fields; scalar/arr/opt: materialize
|