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,12 +2,12 @@ import { GeneratorFailure } from "./core.js";
|
|
|
2
2
|
import { isControlGate } from "../behavior.js";
|
|
3
3
|
import { rustStraightlineInternals } from "./emit-shared-rust.js";
|
|
4
4
|
import { rustTypedInternals } from "./emit-shared-rust-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 } from "./straightline.js";
|
|
7
7
|
import { NativeExprCompiler } from "./native-expr.js";
|
|
8
8
|
import { buildAsyncPlans } from "./async-plan.js";
|
|
9
9
|
const { sanitize, rustStrLit } = rustStraightlineInternals;
|
|
10
|
-
const { rustFieldName, renderTypeRef, typedCell, typedFieldAccess, serializeTyped, emitTypeDecls, emitDefaults, emitSerializers, emitTypedValue, emitMarshallers, emitMarshalHelpers, materializeExpr, } = rustTypedInternals;
|
|
10
|
+
const { typeRefIsCopy, rustFieldName, renderTypeRef, typedCell, typedFieldAccess, serializeTyped, emitTypeDecls, emitDefaults, emitSerializers, emitTypedValue, emitMarshallers, emitMarshalHelpers, materializeExpr, } = rustTypedInternals;
|
|
11
11
|
/** rustErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
|
|
12
12
|
* plane (runtime-free). The covered module declares a local `BehaviorError` (see emitBehaviorErrorType);
|
|
13
13
|
* the runner returns `Result<_, BehaviorError>` over THAT local type. This REPLACES the old
|
|
@@ -135,18 +135,13 @@ function inferPortType(node, inputPorts, asBinding) {
|
|
|
135
135
|
}
|
|
136
136
|
return "Value";
|
|
137
137
|
}
|
|
138
|
+
// a ref to a REQUIRED scalar input port → that scalar. An OPTIONAL port has no required-scalar
|
|
139
|
+
// kind (its native type is Option<T>) and is lowered by the opt lane ahead of this — reaching here
|
|
140
|
+
// with one means the shape is uncovered, and "Value" routes it to the caller's loud fail-closed.
|
|
138
141
|
if (e.path.length === 1) {
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
return "String"; // literal = constrained String.
|
|
143
|
-
if (s.type === "int")
|
|
144
|
-
return "i64";
|
|
145
|
-
if (s.type === "float" || s.type === "number")
|
|
146
|
-
return "f64"; // `"number"` = the float channel.
|
|
147
|
-
if (s.type === "bool")
|
|
148
|
-
return "bool";
|
|
149
|
-
}
|
|
142
|
+
const sc = inputScalarKind(inputPorts[e.path[0]]);
|
|
143
|
+
if (sc !== undefined)
|
|
144
|
+
return sc;
|
|
150
145
|
}
|
|
151
146
|
return "Value";
|
|
152
147
|
}
|
|
@@ -180,6 +175,12 @@ function staticStringArrElems(node) {
|
|
|
180
175
|
* fallback on the native module). `where` names the emission site for the error.
|
|
181
176
|
*/
|
|
182
177
|
function portFieldRustType(node, inputPorts, where = "port", asBinding, plan, typedNodes) {
|
|
178
|
+
// A port reading an OPTIONAL input port lowers through the shared native-expr compiler (the
|
|
179
|
+
// Option<T> pass-through / the coalesce default). It runs FIRST so an optional input can never fall
|
|
180
|
+
// into the required-scalar inference below (which has no way to say "absent").
|
|
181
|
+
const optPort = optPortCompileR(node, inputPorts, where, plan);
|
|
182
|
+
if (optPort !== null)
|
|
183
|
+
return renderTypeRef(optPort.ref);
|
|
183
184
|
if (staticStringArrElems(node) !== null)
|
|
184
185
|
return "Vec<&'static str>";
|
|
185
186
|
// #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → Vec<ElemT>.
|
|
@@ -202,10 +203,61 @@ function portFieldRustType(node, inputPorts, where = "port", asBinding, plan, ty
|
|
|
202
203
|
return renderTypeRef(composite);
|
|
203
204
|
const ft = inferPortType(node, inputPorts, asBinding);
|
|
204
205
|
if (ft === "Value") {
|
|
205
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90 / runtime-free): ${where} '${JSON.stringify(node)}' does not lower to a native Rust type (would need a boxed Value). The covered read plane is runtime-free and admits NO boxed escape
|
|
206
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90 / runtime-free): ${where} '${JSON.stringify(node)}' does not lower to a native Rust type (would need a boxed Value). The covered read plane is runtime-free and admits NO boxed escape. ${RUST_STATIC_PORT_FORMS} Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
|
|
206
207
|
}
|
|
207
208
|
return ft;
|
|
208
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* optPortCompileR — the OPTIONAL-input port lane (rust). A component may declare an input port
|
|
212
|
+
* `{opt:T}` (PortSchema `required:false`); the only native representation of "the value is absent" is
|
|
213
|
+
* `Option<T>`, so such a port never lowers through the required-scalar inference. This lane compiles the
|
|
214
|
+
* port expression with the SHARED runtime-free native-expr compiler (native-expr.ts — the same traversal,
|
|
215
|
+
* type-inference and fail-closed discipline the go twin uses, so the two agree by construction):
|
|
216
|
+
*
|
|
217
|
+
* - `opt($.x)` / `$.x` at port position → `Option<T>` (the leaf receives the absent value as null,
|
|
218
|
+
* exactly as run_behavior's `evalPorts` passes it).
|
|
219
|
+
* - `coalesce(opt($.x), <literal>)` → `in_.x.unwrap_or(<literal>)` — a native `T`, no boxed Value.
|
|
220
|
+
* - `ne(opt($.x), null)` / `eq(…, null)` → the native presence test (`is_some()` / `is_none()`).
|
|
221
|
+
*
|
|
222
|
+
* Returns null when the port expression reads NO optional input (the caller keeps its existing lowering).
|
|
223
|
+
* A port that DOES read one but does not compile — or that compiles to a fallible expression, which this
|
|
224
|
+
* position cannot hoist — FAILS CLOSED loudly rather than degrade to a required type or a zero value.
|
|
225
|
+
*/
|
|
226
|
+
function optPortCompileR(node, inputPorts, where, plan) {
|
|
227
|
+
if (plan === undefined)
|
|
228
|
+
return null;
|
|
229
|
+
if (!exprReadsOptionalInput(node, inputPorts))
|
|
230
|
+
return null;
|
|
231
|
+
const resolveHead = (head) => rustInputHeadRef(head, { inputPorts }, plan);
|
|
232
|
+
let c;
|
|
233
|
+
try {
|
|
234
|
+
c = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compilePort(node);
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
if (!(e instanceof GeneratorFailure))
|
|
238
|
+
throw e;
|
|
239
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} ${JSON.stringify(node)} reads an OPTIONAL input port but does not lower to a native Rust value: ${e.message}`);
|
|
240
|
+
}
|
|
241
|
+
if (c.stmts.length > 0)
|
|
242
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust 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.`);
|
|
243
|
+
return c;
|
|
244
|
+
}
|
|
245
|
+
/** exprReadsOptionalInput — does this port expression reference an OPTIONAL (`required:false`) input
|
|
246
|
+
* port? Language-neutral scan for `ref`/`refOpt` heads bound to an optional port. Decides ONLY which
|
|
247
|
+
* lane a port takes; the lane itself then proves the lowering (or fails closed). */
|
|
248
|
+
function exprReadsOptionalInput(node, inputPorts) {
|
|
249
|
+
if (node === null || typeof node !== "object")
|
|
250
|
+
return false;
|
|
251
|
+
if (Array.isArray(node))
|
|
252
|
+
return node.some((el) => exprReadsOptionalInput(el, inputPorts));
|
|
253
|
+
const rec = node;
|
|
254
|
+
const keys = Object.keys(rec);
|
|
255
|
+
if (keys.length === 1 && (keys[0] === "ref" || keys[0] === "refOpt")) {
|
|
256
|
+
const path = rec[keys[0]];
|
|
257
|
+
return Array.isArray(path) && typeof path[0] === "string" && inputPortIsOptional(inputPorts?.[path[0]]);
|
|
258
|
+
}
|
|
259
|
+
return keys.some((k) => exprReadsOptionalInput(rec[k], inputPorts));
|
|
260
|
+
}
|
|
209
261
|
/** portCompositeRef — the full TypeRef of a port that is a `{ref:[...]}` into a `$as` element field OR
|
|
210
262
|
* an input port whose type is a MAP or NAMED struct (a non-scalar covered field). Returns null when the
|
|
211
263
|
* node is not such a ref (so the caller falls through to the scalar/array inference). #108-map: enables a
|
|
@@ -393,21 +445,40 @@ function makeRustExprBackend(resolveHead, plan) {
|
|
|
393
445
|
// position (a bare `None` can be ambiguous; the explicit turbofish keeps it a fully-typed value).
|
|
394
446
|
optNone: (innerTy) => `Option::<${innerTy}>::None`,
|
|
395
447
|
// opt (`Option<T>`) null-presence test: present = `expr.is_some()`, absent = `expr.is_none()`.
|
|
396
|
-
|
|
397
|
-
|
|
448
|
+
// A presence test only READS the discriminant, so an owning clone of the operand (which a head
|
|
449
|
+
// resolver adds so the value can be moved into a port) is dropped here — testing `Option<String>`
|
|
450
|
+
// must not heap-allocate. Mirrors the go twin's zero-cost `!= nil`.
|
|
451
|
+
optIsSome: (expr) => `${rustStripTrailingClone(expr)}.is_some()`,
|
|
452
|
+
optIsNone: (expr) => `${rustStripTrailingClone(expr)}.is_none()`,
|
|
453
|
+
// opt (`Option<T>`) defaulted to a PURE default — it cannot fail and has no effect, so its
|
|
454
|
+
// evaluation order is UNOBSERVABLE and both forms below are correct (a FALLIBLE default, where
|
|
455
|
+
// laziness IS observable, goes to optCoalesceGuard instead). `unwrap_or` is EAGER — fine for a plain
|
|
456
|
+
// literal/copy. A default that allocates (an owned `String`, a struct/vec literal) uses
|
|
457
|
+
// `unwrap_or_else`, whose closure defers the allocation to the absent branch: not a semantic
|
|
458
|
+
// requirement here, but pointless work otherwise — and what clippy `or_fun_call` asks for.
|
|
459
|
+
optUnwrapOr: (optExpr, defaultExpr, _innerTy) => /^[A-Za-z0-9_.:]+$/.test(defaultExpr) ? `${optExpr}.unwrap_or(${defaultExpr})` : `${optExpr}.unwrap_or_else(|| ${defaultExpr})`,
|
|
460
|
+
// opt defaulted to a FALLIBLE default — a `match`, NOT a closure: the default's hoisted `?` early
|
|
461
|
+
// returns must propagate out of the ENCLOSING fn, which `unwrap_or_else`'s closure would swallow
|
|
462
|
+
// (it would try to return from the closure). Only the None arm evaluates the default.
|
|
463
|
+
optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
|
|
464
|
+
`let ${temp}: ${ty} = match ${optExpr} {`,
|
|
465
|
+
` Some(${bindTemp}) => ${bindTemp},`,
|
|
466
|
+
` None => { ${rustBlockBody(dStmts, dExpr)} }`,
|
|
467
|
+
`};`,
|
|
468
|
+
],
|
|
398
469
|
ternary: (cond, t, e, _ty) => `(if ${rustStripOuterParens(cond)} { ${rustStripOuterParens(t)} } else { ${rustStripOuterParens(e)} })`,
|
|
399
470
|
shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
|
|
400
471
|
// and: let temp = if left { <rStmts> rExpr } else { false }; or: if left { true } else { <rStmts> rExpr }.
|
|
401
472
|
// build a block that runs rStmts then yields rExpr (parens stripped — block-tail is not a sub-expr).
|
|
402
|
-
const rblock = `{ ${rStmts
|
|
473
|
+
const rblock = `{ ${rustBlockBody(rStmts, rExpr)} }`;
|
|
403
474
|
if (op === "and") {
|
|
404
475
|
return [`let ${temp}: bool = if ${rustStripOuterParens(left)} ${rblock} else { false };`];
|
|
405
476
|
}
|
|
406
477
|
return [`let ${temp}: bool = if ${rustStripOuterParens(left)} { true } else ${rblock};`];
|
|
407
478
|
},
|
|
408
479
|
condGuard: (temp, ty, cond, tStmts, tExpr, eStmts, eExpr) => {
|
|
409
|
-
const tblock = `{ ${tStmts
|
|
410
|
-
const eblock = `{ ${eStmts
|
|
480
|
+
const tblock = `{ ${rustBlockBody(tStmts, tExpr)} }`;
|
|
481
|
+
const eblock = `{ ${rustBlockBody(eStmts, eExpr)} }`;
|
|
411
482
|
return [`let ${temp}: ${ty} = if ${rustStripOuterParens(cond)} ${tblock} else ${eblock};`];
|
|
412
483
|
},
|
|
413
484
|
};
|
|
@@ -416,6 +487,17 @@ function makeRustExprBackend(resolveHead, plan) {
|
|
|
416
487
|
function rustSnake(name) {
|
|
417
488
|
return name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").replace(/([0-9])([a-z])/g, "$1_$2").toLowerCase();
|
|
418
489
|
}
|
|
490
|
+
/** rustBlockBody — the body of a block-expression that runs `stmts` then yields `expr`. A trailing
|
|
491
|
+
* `let t = E; t` is collapsed to `E`: binding a temp only to return it on the next line is clippy's
|
|
492
|
+
* `let_and_return` (an error under `-D warnings`), and the hoisted-fallible shape produces exactly that
|
|
493
|
+
* whenever the block's tail IS the hoisted temp. */
|
|
494
|
+
function rustBlockBody(stmts, expr) {
|
|
495
|
+
const last = stmts[stmts.length - 1];
|
|
496
|
+
const m = last !== undefined ? /^let\s+([A-Za-z0-9_]+)(?::\s*[^=]+)?\s*=\s*(.+);$/.exec(last) : null;
|
|
497
|
+
if (m !== null && m[1] === expr)
|
|
498
|
+
return [...stmts.slice(0, -1), m[2]].join(" ");
|
|
499
|
+
return [...stmts, rustStripOuterParens(expr)].join(" ");
|
|
500
|
+
}
|
|
419
501
|
/** strip ONE redundant outer paren pair from an expr (clippy `if (cond)` unused_parens). Only when the
|
|
420
502
|
* whole string is a single balanced `( … )` — a `(a) && (b)` stays untouched. */
|
|
421
503
|
function rustStripOuterParens(expr) {
|
|
@@ -433,6 +515,12 @@ function rustStripOuterParens(expr) {
|
|
|
433
515
|
}
|
|
434
516
|
return expr.slice(1, -1);
|
|
435
517
|
}
|
|
518
|
+
/** Drop a TRAILING `.clone()` from an expression — for a position that only reads the value (no move),
|
|
519
|
+
* so an owning clone added by a head resolver is pure waste. Only a clone at the very END is dropped: a
|
|
520
|
+
* `.clone()` mid-path (`t_x.borrow().clone().field`) still owns the field access that follows it. */
|
|
521
|
+
function rustStripTrailingClone(expr) {
|
|
522
|
+
return expr.endsWith(".clone()") ? expr.slice(0, -".clone()".length) : expr;
|
|
523
|
+
}
|
|
436
524
|
/** Rust float literal (finite; integral gets `.0`). */
|
|
437
525
|
function rustFloatLitExpr(n) {
|
|
438
526
|
if (Number.isInteger(n))
|
|
@@ -667,9 +755,13 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
667
755
|
lines.push(`}`);
|
|
668
756
|
return lines.join("\n");
|
|
669
757
|
}
|
|
670
|
-
/** mapElemMaterializerName — the augmented-element copier for a covered map...into node.
|
|
671
|
-
|
|
672
|
-
|
|
758
|
+
/** mapElemMaterializerName — the augmented-element copier for a covered map...into node.
|
|
759
|
+
* QUALIFIED by component name: two components may each carry a map node at the SAME
|
|
760
|
+
* recorder-scan index (e.g. both `…authorFollows` and `…groupsNested` have a map at `n4`),
|
|
761
|
+
* and a node-id-only name would collide at module scope (E0428). Mirrors the Go emitter
|
|
762
|
+
* (`materializeMapElemNR_<comp>_<node>`) and the sibling `decode_row_<comp>_<node>` helper. */
|
|
763
|
+
function mapElemMaterializerName(compName, nodeId) {
|
|
764
|
+
return `materialize_map_elem_${sanitize(compName)}_${typedCell(nodeId)}`;
|
|
673
765
|
}
|
|
674
766
|
/** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType`, normalizing the two IR
|
|
675
767
|
* conventions to the one array shape every downstream site assumes (`typedNodes.get(mapId)` is an arr):
|
|
@@ -725,7 +817,7 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
|
725
817
|
const intoField = decl.fields.find((f) => f.name === intoKey);
|
|
726
818
|
const overFields = decl.fields.filter((f) => f.name !== intoKey);
|
|
727
819
|
const overElemTy = mapOverElemType(comp, n, typedNodes, plan);
|
|
728
|
-
const fn = mapElemMaterializerName(n.id);
|
|
820
|
+
const fn = mapElemMaterializerName(comp.name, n.id);
|
|
729
821
|
const elemRowGo = rawElemStructName(comp.name, n.id);
|
|
730
822
|
const intoRowRef = mapElemRowRef(n, arrRef, plan);
|
|
731
823
|
const inits = [];
|
|
@@ -855,14 +947,20 @@ function numLiteralRustExpr(node) {
|
|
|
855
947
|
// fractional / exponent literal → checked f64 (finite, guaranteed above).
|
|
856
948
|
return `Value::Float(${node}f64)`;
|
|
857
949
|
}
|
|
858
|
-
/** A port node is static (literal / ref / concat of those / static arr / bare number literal
|
|
859
|
-
* resolvable to a native
|
|
860
|
-
|
|
950
|
+
/** A port node is static (literal / ref / concat of those / static arr / bare number literal / an
|
|
951
|
+
* optional-input read) — resolvable to a native value with no Obj / no evaluate interpreter. This is the
|
|
952
|
+
* SHAPE gate that decides native ELIGIBILITY; the lowering itself (portFieldRustType / emitPortInit) is
|
|
953
|
+
* what proves each shape, and fails closed loudly on one it cannot lower. */
|
|
954
|
+
function portIsStatic(node, inputPorts) {
|
|
861
955
|
const arr = staticArrElems(node);
|
|
862
956
|
if (arr !== null)
|
|
863
|
-
return arr.every(portIsStatic);
|
|
957
|
+
return arr.every((el) => portIsStatic(el, inputPorts));
|
|
864
958
|
if (numLiteralRustExpr(node) !== null)
|
|
865
959
|
return true; // bare number literal (e.g. Query `limit`)
|
|
960
|
+
// An optional-input read (`opt($.x)` / `coalesce(opt($.x), 20)` / `ne(opt($.x), null)`) is a
|
|
961
|
+
// statically-resolvable port — the opt lane lowers it (or fails closed with the specific reason).
|
|
962
|
+
if (inputPorts !== undefined && exprReadsOptionalInput(node, inputPorts))
|
|
963
|
+
return true;
|
|
866
964
|
const e = classifyExpr(node);
|
|
867
965
|
switch (e.kind) {
|
|
868
966
|
case "str":
|
|
@@ -871,7 +969,7 @@ function portIsStatic(node) {
|
|
|
871
969
|
case "ref":
|
|
872
970
|
return true;
|
|
873
971
|
case "concat":
|
|
874
|
-
return e.parts.every((p) => portIsStatic(reconstructR(p)));
|
|
972
|
+
return e.parts.every((p) => portIsStatic(reconstructR(p), inputPorts));
|
|
875
973
|
default:
|
|
876
974
|
return false;
|
|
877
975
|
}
|
|
@@ -940,7 +1038,7 @@ function coveredMapNode(n, bodyIds, comp) {
|
|
|
940
1038
|
}
|
|
941
1039
|
const ports = m.ports;
|
|
942
1040
|
for (const p of Object.keys(ports))
|
|
943
|
-
if (!portIsStatic(ports[p]))
|
|
1041
|
+
if (!portIsStatic(ports[p], comp.inputPorts))
|
|
944
1042
|
return false;
|
|
945
1043
|
return true;
|
|
946
1044
|
}
|
|
@@ -967,7 +1065,7 @@ function coveredFanoutNode(n, bodyIds, comp) {
|
|
|
967
1065
|
}
|
|
968
1066
|
const ports = f.ports;
|
|
969
1067
|
for (const p of Object.keys(ports))
|
|
970
|
-
if (!portIsStatic(ports[p]))
|
|
1068
|
+
if (!portIsStatic(ports[p], comp.inputPorts))
|
|
971
1069
|
return false;
|
|
972
1070
|
return true;
|
|
973
1071
|
}
|
|
@@ -1078,7 +1176,7 @@ function coverageRejectReason(comp) {
|
|
|
1078
1176
|
if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
1079
1177
|
return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
|
|
1080
1178
|
for (const p of Object.keys(cr.ports))
|
|
1081
|
-
if (portIsStatic(cr.ports[p]) === false)
|
|
1179
|
+
if (portIsStatic(cr.ports[p], comp.inputPorts) === false)
|
|
1082
1180
|
return `node '${n.id}' port '${p}' is not statically resolvable`;
|
|
1083
1181
|
}
|
|
1084
1182
|
if (!outputIsNativeLowerable(comp))
|
|
@@ -1106,44 +1204,31 @@ function assertTypedCoverage(comp) {
|
|
|
1106
1204
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust 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 rust-straightline-native (boxed result).`);
|
|
1107
1205
|
}
|
|
1108
1206
|
// ── typed input struct (InNR<Comp>) + NATIVE scalar port lowering ────────────────────────────
|
|
1109
|
-
/**
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
return `Vec<${renderTypeRef(deriveTypeRef(et, plan))}>`;
|
|
1132
|
-
return "Value";
|
|
1133
|
-
}
|
|
1134
|
-
case "map": {
|
|
1135
|
-
// an input MAP port (dynamic string keys, declared value elemType) lowers to a native BTreeMap.
|
|
1136
|
-
const et = schema.elemType;
|
|
1137
|
-
if (et !== undefined && plan !== undefined)
|
|
1138
|
-
return `std::collections::BTreeMap<String, ${renderTypeRef(deriveTypeRef(et, plan))}>`;
|
|
1139
|
-
return "Value";
|
|
1140
|
-
}
|
|
1141
|
-
default:
|
|
1142
|
-
return "Value";
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
/** The scalar kind an input port declares, or undefined if it is not a native scalar. */
|
|
1207
|
+
/**
|
|
1208
|
+
* inputPortRustType — the native Rust type for an input port's declared type. Scalars lower to the
|
|
1209
|
+
* concrete scalar; `array`/`map` with a declared elemType to `Vec<ElemT>` / `BTreeMap<String, ElemT>`;
|
|
1210
|
+
* an OPTIONAL port (`{opt:T}` → `required:false`) to `Option<T>` — the native representation of "the
|
|
1211
|
+
* value is absent". A declared type with no native lowering stays the boxed `Value`.
|
|
1212
|
+
*
|
|
1213
|
+
* An OPTIONAL port whose inner type has no native lowering FAILS CLOSED: `Option<Value>` would be a
|
|
1214
|
+
* boxed escape, and emitting the bare inner type would silently turn "absent" into a zero value. A port
|
|
1215
|
+
* whose optionality cannot be represented is never silently coerced.
|
|
1216
|
+
*/
|
|
1217
|
+
function inputPortRustType(schema, plan, where = "input port") {
|
|
1218
|
+
const ref = inputPortTypeRef(schema, plan);
|
|
1219
|
+
if (ref !== undefined)
|
|
1220
|
+
return renderTypeRef(ref);
|
|
1221
|
+
if (inputPortIsOptional(schema))
|
|
1222
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Rust lowering, so "absent" has no native representation (an Option<Value> would be a boxed 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.`);
|
|
1223
|
+
return "Value";
|
|
1224
|
+
}
|
|
1225
|
+
/** The scalar kind a REQUIRED input port declares, or undefined when it is not a required native scalar.
|
|
1226
|
+
* An OPTIONAL port is deliberately NOT a scalar kind here: its native type is `Option<T>`, and the
|
|
1227
|
+
* callers of this function build REQUIRED scalar slots (`in_.<field>` read straight into a non-opt port
|
|
1228
|
+
* field). Answering "i64" for an `{opt:"int"}` port is exactly how "absent" would silently become `0`. */
|
|
1146
1229
|
function inputScalarKind(schema) {
|
|
1230
|
+
if (inputPortIsOptional(schema))
|
|
1231
|
+
return undefined;
|
|
1147
1232
|
switch (schema?.type) {
|
|
1148
1233
|
case "string":
|
|
1149
1234
|
case "literal": // a `"literal"` union is a constrained String (see inputPortRustType).
|
|
@@ -1168,7 +1253,7 @@ function emitInStruct(comp, plan) {
|
|
|
1168
1253
|
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\n#[derive(Default)]\npub struct ${name};`;
|
|
1169
1254
|
}
|
|
1170
1255
|
const fields = ports
|
|
1171
|
-
.map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p], plan)}, // ${JSON.stringify(p)}`)
|
|
1256
|
+
.map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`)}, // ${JSON.stringify(p)}`)
|
|
1172
1257
|
.join("\n");
|
|
1173
1258
|
return `// ${name} — the CONCRETE input for '${comp.name}' (fields = inputPorts; typed, consumer-built —
|
|
1174
1259
|
// NO generic Value slice, NO per-field boxing crosses the covered read boundary).
|
|
@@ -1177,6 +1262,9 @@ pub struct ${name} {
|
|
|
1177
1262
|
${fields}
|
|
1178
1263
|
}`;
|
|
1179
1264
|
}
|
|
1265
|
+
/** The port forms the STATIC (non-optional) lane lowers — named in its reject message so the message
|
|
1266
|
+
* describes what is actually covered rather than a stale corpus list. */
|
|
1267
|
+
const RUST_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.)";
|
|
1180
1268
|
/**
|
|
1181
1269
|
* emitNativeScalar — lower a static port expr to a NATIVE Rust expression of the concrete `want` scalar
|
|
1182
1270
|
* type, or null if it cannot be statically proven that type. A string literal is native for
|
|
@@ -1250,6 +1338,11 @@ function emitNativeScalar(node, want, resolveRef) {
|
|
|
1250
1338
|
*/
|
|
1251
1339
|
function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan, typedNodes, asBinding, elemBaseExpr) {
|
|
1252
1340
|
const field = portFieldName(pn);
|
|
1341
|
+
// The OPTIONAL-input lane (Option<T> pass-through / coalesce default) — same shared compile as
|
|
1342
|
+
// portFieldRustType, so the field type and its initializer are derived from ONE lowering.
|
|
1343
|
+
const optPort = optPortCompileR(expr, inputPorts ?? {}, `port '${pn}'`, plan);
|
|
1344
|
+
if (optPort !== null)
|
|
1345
|
+
return `${field}: ${optPort.expr}`;
|
|
1253
1346
|
// #108-map: a port that is a ref to a `$as` element MAP/NAMED field (or an input map port) → CLONE the
|
|
1254
1347
|
// typed composite value directly off the element/input (native BTreeMap / struct — ZERO boxed Value).
|
|
1255
1348
|
// Reuses portCompositeRef for the type; the value is the typed field access `.clone()`.
|
|
@@ -1299,7 +1392,7 @@ function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan, typ
|
|
|
1299
1392
|
// FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would re-couple
|
|
1300
1393
|
// the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the component is not
|
|
1301
1394
|
// native-eligible and must regenerate on the boxed straight-line path.
|
|
1302
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90 / runtime-free): port '${pn}' (${JSON.stringify(expr)}) does not lower to a native Rust value of type '${fieldRustType}'. The covered read plane admits NO boxed Value escape
|
|
1395
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90 / runtime-free): port '${pn}' (${JSON.stringify(expr)}) does not lower to a native Rust value of type '${fieldRustType}'. The covered read plane admits NO boxed Value escape. ${RUST_STATIC_PORT_FORMS} Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
|
|
1303
1396
|
}
|
|
1304
1397
|
// ── the combined runner (STRUCT-returning; INLINE sequential; native ports in; concrete row out) ──
|
|
1305
1398
|
/** resolveRefAt for the top-level runner: a ref head is a PRIOR NODE (its typed scalar field) or an
|
|
@@ -1771,7 +1864,7 @@ isAsync = false) {
|
|
|
1771
1864
|
const batch = `${structName}Batch`;
|
|
1772
1865
|
// #108: over element type + the OWNED over-Vec expression (prior-node arr OR input array w/ elemType).
|
|
1773
1866
|
const { elemRef: overElemRef, overExpr } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
1774
|
-
const elemMat = mapElemMaterializerName(node.id);
|
|
1867
|
+
const elemMat = mapElemMaterializerName(comp.name, node.id);
|
|
1775
1868
|
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
1776
1869
|
const asName = m.as;
|
|
1777
1870
|
const portNames = Object.keys(m.ports);
|
|
@@ -2073,24 +2166,29 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
|
|
|
2073
2166
|
lines.push(c);
|
|
2074
2167
|
return lines.join("\n");
|
|
2075
2168
|
}
|
|
2076
|
-
/**
|
|
2077
|
-
*
|
|
2169
|
+
/**
|
|
2170
|
+
* rustInputHeadRef (#108) — resolve an INPUT port head for a native-expr ref (rust). The port's declared
|
|
2171
|
+
* type comes from the shared `inputPortTypeRef` SSoT, so an OPTIONAL port resolves as `{opt:…}` (native
|
|
2172
|
+
* `Option<T>`) and the compiler can see its optionality: a null-compare becomes a real presence test and
|
|
2173
|
+
* a coalesce a real `unwrap_or`, while a REQUIRED-scalar slot fed by an opt fails closed. Exprs are OWNED
|
|
2174
|
+
* (String / Option<String> cloned; Copy scalars as-is), except an array — the only array use inside a
|
|
2175
|
+
* cond/guard expr is `len`, which BORROWS (`&in_.xs`).
|
|
2176
|
+
*/
|
|
2078
2177
|
function rustInputHeadRef(head, comp, plan) {
|
|
2079
2178
|
const schema = comp.inputPorts?.[head];
|
|
2080
|
-
|
|
2179
|
+
const ref = inputPortTypeRef(schema, plan);
|
|
2180
|
+
if (ref === undefined)
|
|
2081
2181
|
return null;
|
|
2082
|
-
const
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
if (
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
}
|
|
2093
|
-
return null;
|
|
2182
|
+
const field = `in_.${rustFieldName(head)}`;
|
|
2183
|
+
return { expr: rustOwnedHeadExpr(field, ref), ref };
|
|
2184
|
+
}
|
|
2185
|
+
/** The OWNED native expression for a head of type `ref` read off a by-value struct field. A String (or an
|
|
2186
|
+
* Option/collection containing one) is cloned; a Copy scalar (and an Option/array of Copy scalars) is
|
|
2187
|
+
* read directly — cloning a Copy value trips clippy's `clone_on_copy`. An array stays borrowed (`len`). */
|
|
2188
|
+
function rustOwnedHeadExpr(field, ref) {
|
|
2189
|
+
if (ref.kind === "arr")
|
|
2190
|
+
return field; // borrowed by `len`
|
|
2191
|
+
return typeRefIsCopy(ref) ? field : `${field}.clone()`;
|
|
2094
2192
|
}
|
|
2095
2193
|
/**
|
|
2096
2194
|
* emitCondArm — the struct-native exec of a covered cond node (#108, rust). Mirrors run_behavior's cond
|
|
@@ -2465,17 +2563,22 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2465
2563
|
const node = n;
|
|
2466
2564
|
const rowT = rawRowStructName(c.name, node.id);
|
|
2467
2565
|
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
2468
|
-
// #129 (TEST glue):
|
|
2469
|
-
//
|
|
2470
|
-
//
|
|
2471
|
-
//
|
|
2472
|
-
//
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2566
|
+
// #129 (TEST glue): support the reference scriptedHandlers' echo forms (PROTOCOL.md §3.5) — the
|
|
2567
|
+
// adapter normalizes `{"echo":"port","port":"X"}` / `{"echo":"items"}` to the port NAME, and the
|
|
2568
|
+
// scripted handler
|
|
2569
|
+
// ECHOES a NATIVE port back as its result. This makes a port-plane pin non-vacuous: the node's
|
|
2570
|
+
// observed result IS the native value that flowed into its ports struct, so a dropped / re-boxed /
|
|
2571
|
+
// zero-defaulted port diverges from run_behavior instead of passing. A port is echoable when its
|
|
2572
|
+
// native type IS the node's outType (that is exactly when the row's `val` can hold it); `ports` is
|
|
2573
|
+
// bound only when at least one port qualifies.
|
|
2574
|
+
const echoable = echoablePorts(c, node, ref, plan, typedNodes);
|
|
2575
|
+
const portsParam = echoable.length > 0 ? "ports" : "_ports";
|
|
2576
|
+
// a Copy port (i64/f64/bool/Option<scalar>) is read directly — cloning it trips clippy clone_on_copy.
|
|
2577
|
+
const echoRead = (p) => `${portsParam}.${portFieldName(p)}${typeRefIsCopy(ref) ? "" : ".clone()"}`;
|
|
2578
|
+
const echoBranch = echoable
|
|
2579
|
+
.map((p) => `\n if echo == ${rustStrLit(p)} {\n return Some(${rowT} { val: ${echoRead(p)}, ..Default::default() });\n }`)
|
|
2580
|
+
.join("");
|
|
2581
|
+
const okBind = echoable.length > 0 ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
|
|
2479
2582
|
methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
|
|
2480
2583
|
let ${okBind} = self.next(${rustStrLit(node.component)})?;
|
|
2481
2584
|
if is_err {
|
|
@@ -2544,7 +2647,14 @@ impl ScriptedNativeRaw {
|
|
|
2544
2647
|
let mut queues = self.queues.lock().unwrap();
|
|
2545
2648
|
let (queue, single) = queues.get_mut(component)?;
|
|
2546
2649
|
let raw = if *single || queue.len() <= 1 { queue[0].clone() } else { queue.remove(0) };
|
|
2547
|
-
|
|
2650
|
+
// PROTOCOL.md §3.5 echo forms, normalized to the port NAME: {"echo":"port","port":"X"} -> "X"
|
|
2651
|
+
// ({"echo":"items"} is already that shape). The generated node method just compares this
|
|
2652
|
+
// against its echoable port names.
|
|
2653
|
+
let echo = match raw.get("echo").and_then(|e| e.as_str()) {
|
|
2654
|
+
Some("port") => raw.get("port").and_then(|p| p.as_str()).unwrap_or("").to_string(),
|
|
2655
|
+
Some(other) => other.to_string(),
|
|
2656
|
+
None => String::new(),
|
|
2657
|
+
};
|
|
2548
2658
|
if let Some(okv) = raw.get("ok") {
|
|
2549
2659
|
let v = behavior_contracts::decode_value(okv).expect("decode ok");
|
|
2550
2660
|
Some((false, String::new(), Some(v), echo))
|
|
@@ -2635,8 +2745,10 @@ ${arms}
|
|
|
2635
2745
|
}
|
|
2636
2746
|
/** emitInDecoder — a TEST-glue decoder `decode_InNR_<comp>(input) -> Result<InNR_<comp>, BehaviorError>`.
|
|
2637
2747
|
* Reads each declared input port from the generic &[(String,Value)] into the concrete field (scalar →
|
|
2638
|
-
* typed read; Value-typed → the raw Value clone). A
|
|
2639
|
-
*
|
|
2748
|
+
* typed read; opt → Some/None; Value-typed → the raw Value clone). A key that is absent, or present as
|
|
2749
|
+
* `Value::Null`, leaves the field's Default — for an OPTIONAL port that is `None` (absent), which is the
|
|
2750
|
+
* same value `refCore` observes for the `null` binding run_behavior is given. Lives ONLY in the observe
|
|
2751
|
+
* companion. */
|
|
2640
2752
|
function emitInDecoder(comp, plan) {
|
|
2641
2753
|
const name = inStructName(comp.name);
|
|
2642
2754
|
const ports = Object.keys(comp.inputPorts ?? {});
|
|
@@ -2658,7 +2770,15 @@ function emitInDecoder(comp, plan) {
|
|
|
2658
2770
|
const et = schema.elemType;
|
|
2659
2771
|
const head = i === 0 ? " if" : " } else if";
|
|
2660
2772
|
lines.push(`${head} k == ${rustStrLit(p)} {`);
|
|
2661
|
-
if ((schema
|
|
2773
|
+
if (inputPortIsOptional(schema)) {
|
|
2774
|
+
// An OPTIONAL port decodes into its native Option<T>. materializeExpr's opt de-box IS the wire
|
|
2775
|
+
// rule: `Value::Null` → None (absent), any other value → Some(<inner>). A key that never appears
|
|
2776
|
+
// keeps the struct Default (None) — the same absent value. Checked FIRST: optionality wraps every
|
|
2777
|
+
// inner type (an optional array is Option<Vec<T>>, not Vec<T>).
|
|
2778
|
+
const optRef = inputPortTypeRef(schema, plan);
|
|
2779
|
+
lines.push(` in_.${field} = ${materializeExpr("v", optRef, plan)};`);
|
|
2780
|
+
}
|
|
2781
|
+
else if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
2662
2782
|
// #108: decode the boxed Value::Arr into the native Vec<ElemT> (test glue — materialize each elem).
|
|
2663
2783
|
const elemRef = deriveTypeRef(et, plan);
|
|
2664
2784
|
const elemTy = renderTypeRef(elemRef);
|
|
@@ -2696,6 +2816,16 @@ function emitInDecoder(comp, plan) {
|
|
|
2696
2816
|
lines.push(`}`);
|
|
2697
2817
|
return lines.join("\n");
|
|
2698
2818
|
}
|
|
2819
|
+
/** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
|
|
2820
|
+
* declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row's `val`. A
|
|
2821
|
+
* port with a different type is not echoable (the row could not hold it), and a NAMED outType has no
|
|
2822
|
+
* `val` at all (its row carries the struct's fields flattened — see emitDecodeRow). */
|
|
2823
|
+
function echoablePorts(comp, node, ref, plan, typedNodes) {
|
|
2824
|
+
if (ref.kind === "named")
|
|
2825
|
+
return [];
|
|
2826
|
+
const want = renderTypeRef(ref);
|
|
2827
|
+
return Object.keys(node.ports).filter((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}'`, undefined, plan, typedNodes) === want);
|
|
2828
|
+
}
|
|
2699
2829
|
/** emitDecodeRow — emit (once, deduped) a decoder `decode_row_<comp>_<node><suffix>(v: &Value) -> <rowT>`
|
|
2700
2830
|
* that materializes the scripted ok Value into the concrete row struct. Named: reuse the Value->struct
|
|
2701
2831
|
* marshaller + move fields; scalar/arr/opt: materialize into .val. TEST glue only. */
|