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
|
@@ -6,6 +6,7 @@ import { buildTypePlan, deriveTypeRef, findDecl, inputPortIsOptional, inputPortT
|
|
|
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
|
+
import { buildDeBoxPlan, notationOfRef } from "./emit-shared-debox.js";
|
|
9
10
|
const { sanitize, rustStrLit } = rustStraightlineInternals;
|
|
10
11
|
const { typeRefIsCopy, rustFieldName, renderTypeRef, typedCell, typedFieldAccess, serializeTyped, emitTypeDecls, emitDefaults, emitSerializers, emitTypedValue, emitMarshallers, emitMarshalHelpers, materializeExpr, } = rustTypedInternals;
|
|
11
12
|
/** rustErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
|
|
@@ -20,25 +21,71 @@ function rustErr(code, msgExpr) {
|
|
|
20
21
|
* fail closed with ZERO bc-runtime import. Codes match run_behavior verbatim (byte-equal). `code()` /
|
|
21
22
|
* `failure_code()` expose the stable code without a bc-runtime type. */
|
|
22
23
|
function emitBehaviorErrorType() {
|
|
23
|
-
return `//
|
|
24
|
+
return `// ErrorKind — what went wrong (the closed set of scp-error.md). A concrete enum: the covered plane
|
|
25
|
+
// carries no strings-as-tags and no dynamic kind lookup.
|
|
26
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
27
|
+
pub enum ErrorKind {
|
|
28
|
+
TypeMismatch,
|
|
29
|
+
MissingField,
|
|
30
|
+
Overflow,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl ErrorKind {
|
|
34
|
+
pub fn as_str(self) -> &'static str {
|
|
35
|
+
match self {
|
|
36
|
+
ErrorKind::TypeMismatch => "typeMismatch",
|
|
37
|
+
ErrorKind::MissingField => "missingField",
|
|
38
|
+
ErrorKind::Overflow => "overflow",
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ErrorDetail — the structured, recoverable payload a failure carries (scp-error.md "The Error
|
|
44
|
+
// Value"). The LEAF produces it at the wire boundary (it is the only party holding both the declared
|
|
45
|
+
// type and the raw wire datum); the runner transports it verbatim. Concrete fields — no boxed Value,
|
|
46
|
+
// no serialized blob in a string: \`expected_type\` is Portable Type Notation, a rendering of a
|
|
47
|
+
// STATICALLY DECLARED type, so nothing walks a type at runtime.
|
|
48
|
+
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
|
49
|
+
pub struct ErrorDetail {
|
|
50
|
+
pub kind: Option<ErrorKind>,
|
|
51
|
+
pub model: Option<String>,
|
|
52
|
+
pub field: Option<String>,
|
|
53
|
+
pub expected_type: Option<String>,
|
|
54
|
+
pub actual_wire_type: Option<String>,
|
|
55
|
+
pub raw_value: Option<String>,
|
|
56
|
+
pub context: std::collections::BTreeMap<String, String>,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
|
|
24
60
|
// covered runner returns \`Result<T, BehaviorError>\` over THIS local type instead of a bc-runtime
|
|
25
61
|
// failure, so the fully-covered module imports ZERO bc runtime. Codes match run_behavior verbatim
|
|
26
|
-
// (byte-equal).
|
|
27
|
-
//
|
|
62
|
+
// (byte-equal). \`detail\` carries the leaf's structured Error Value across the seam so a caller can
|
|
63
|
+
// log / skip / repair / migrate rather than parse a message. The observe companion (test glue, a
|
|
64
|
+
// super:: submodule) bridges this local error to the bc-runtime failure at the observe boundary —
|
|
65
|
+
// the covered module itself stays runtime-free.
|
|
28
66
|
#[derive(Debug, Clone)]
|
|
29
67
|
pub struct BehaviorError {
|
|
30
68
|
pub code: String,
|
|
31
69
|
pub message: String,
|
|
70
|
+
pub detail: Option<Box<ErrorDetail>>,
|
|
32
71
|
}
|
|
33
72
|
|
|
34
73
|
impl BehaviorError {
|
|
35
74
|
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
|
|
36
|
-
BehaviorError { code: code.into(), message: message.into() }
|
|
75
|
+
BehaviorError { code: code.into(), message: message.into(), detail: None }
|
|
76
|
+
}
|
|
77
|
+
/// The same failure carrying the leaf's structured Error Value.
|
|
78
|
+
pub fn with_detail(code: impl Into<String>, message: impl Into<String>, detail: ErrorDetail) -> Self {
|
|
79
|
+
BehaviorError { code: code.into(), message: message.into(), detail: Some(Box::new(detail)) }
|
|
37
80
|
}
|
|
38
81
|
/// The stable failure code (byte-equal to run_behavior) WITHOUT a bc-runtime type.
|
|
39
82
|
pub fn code(&self) -> &str {
|
|
40
83
|
&self.code
|
|
41
84
|
}
|
|
85
|
+
/// The structured payload, if this failure is about a datum.
|
|
86
|
+
pub fn detail(&self) -> Option<&ErrorDetail> {
|
|
87
|
+
self.detail.as_deref()
|
|
88
|
+
}
|
|
42
89
|
}
|
|
43
90
|
|
|
44
91
|
impl std::fmt::Display for BehaviorError {
|
|
@@ -46,7 +93,75 @@ impl std::fmt::Display for BehaviorError {
|
|
|
46
93
|
write!(f, "{}: {}", self.code, self.message)
|
|
47
94
|
}
|
|
48
95
|
}
|
|
49
|
-
impl std::error::Error for BehaviorError {}
|
|
96
|
+
impl std::error::Error for BehaviorError {}
|
|
97
|
+
|
|
98
|
+
// op_failed — wrap a leaf failure as the node's Failure under its Error Policy Kind, carrying the
|
|
99
|
+
// leaf's structured detail through unchanged (the runner transports the Error Value; it does not
|
|
100
|
+
// synthesize or re-interpret it).
|
|
101
|
+
#[allow(dead_code)]
|
|
102
|
+
fn op_failed(node: &str, policy: &str, e: BehaviorError) -> BehaviorError {
|
|
103
|
+
BehaviorError {
|
|
104
|
+
code: "OP_FAILED".to_string(),
|
|
105
|
+
message: format!("operation '{node}' failed under '{policy}' policy: {}", e.message),
|
|
106
|
+
detail: e.detail,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// de_type_mismatch / de_missing_field / de_overflow — the de-box mismatch failures the generated decode
|
|
111
|
+
// produces (scp-error.md "The Error Value"). Codes are byte-equal to the runtime outType check
|
|
112
|
+
// (TYPE_MISMATCH / MISSING_PROP) so the covered read stays equivalent to run_behavior; detail.kind
|
|
113
|
+
// distinguishes typeMismatch / missingField / overflow.
|
|
114
|
+
#[allow(dead_code)]
|
|
115
|
+
fn de_type_mismatch(model: &str, field: &str, expected: &str, actual_wire: String, raw: String) -> BehaviorError {
|
|
116
|
+
BehaviorError::with_detail(
|
|
117
|
+
"TYPE_MISMATCH",
|
|
118
|
+
format!("node '{model}': {field}: expected {expected}, got {actual_wire}"),
|
|
119
|
+
ErrorDetail {
|
|
120
|
+
kind: Some(ErrorKind::TypeMismatch),
|
|
121
|
+
model: Some(model.to_string()),
|
|
122
|
+
field: Some(field.to_string()),
|
|
123
|
+
expected_type: Some(expected.to_string()),
|
|
124
|
+
actual_wire_type: Some(actual_wire),
|
|
125
|
+
raw_value: Some(raw),
|
|
126
|
+
context: std::collections::BTreeMap::new(),
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[allow(dead_code)]
|
|
132
|
+
fn de_missing_field(model: &str, field: &str, expected: &str) -> BehaviorError {
|
|
133
|
+
BehaviorError::with_detail(
|
|
134
|
+
"MISSING_PROP",
|
|
135
|
+
format!("node '{model}': {field}: required field is absent"),
|
|
136
|
+
ErrorDetail {
|
|
137
|
+
kind: Some(ErrorKind::MissingField),
|
|
138
|
+
model: Some(model.to_string()),
|
|
139
|
+
field: Some(field.to_string()),
|
|
140
|
+
expected_type: Some(expected.to_string()),
|
|
141
|
+
actual_wire_type: None,
|
|
142
|
+
raw_value: None,
|
|
143
|
+
context: std::collections::BTreeMap::new(),
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[allow(dead_code)]
|
|
149
|
+
fn de_overflow(model: &str, field: &str, expected: &str, actual_wire: String, raw: String) -> BehaviorError {
|
|
150
|
+
let message = format!("node '{model}': {field}: {raw} is outside the range of {expected}");
|
|
151
|
+
BehaviorError::with_detail(
|
|
152
|
+
"TYPE_MISMATCH",
|
|
153
|
+
message,
|
|
154
|
+
ErrorDetail {
|
|
155
|
+
kind: Some(ErrorKind::Overflow),
|
|
156
|
+
model: Some(model.to_string()),
|
|
157
|
+
field: Some(field.to_string()),
|
|
158
|
+
expected_type: Some(expected.to_string()),
|
|
159
|
+
actual_wire_type: Some(actual_wire),
|
|
160
|
+
raw_value: Some(raw),
|
|
161
|
+
context: std::collections::BTreeMap::new(),
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
}`;
|
|
50
165
|
}
|
|
51
166
|
function portFieldName(wire) {
|
|
52
167
|
const safe = wire.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase();
|
|
@@ -58,14 +173,6 @@ function pascal(s) {
|
|
|
58
173
|
function portsStructName(compName, nodeId) {
|
|
59
174
|
return `PortsNR${pascal(compName)}${pascal(nodeId)}`;
|
|
60
175
|
}
|
|
61
|
-
/** RawRowNR<Comp><Node> — the concrete per-node handler-result row struct name. */
|
|
62
|
-
function rawRowStructName(compName, nodeId) {
|
|
63
|
-
return `RawRowNR${pascal(compName)}${pascal(nodeId)}`;
|
|
64
|
-
}
|
|
65
|
-
/** RawElemNR<Comp><Node> — the concrete per-ELEMENT row struct name for a covered map node. */
|
|
66
|
-
function rawElemStructName(compName, nodeId) {
|
|
67
|
-
return `RawElemNR${pascal(compName)}${pascal(nodeId)}`;
|
|
68
|
-
}
|
|
69
176
|
/** HandlerNR<Comp> — the per-component concrete handler trait name (one node_* method per node). */
|
|
70
177
|
function handlerTraitName(compName) {
|
|
71
178
|
return `HandlerNR${pascal(compName)}`;
|
|
@@ -78,6 +185,204 @@ function nodeMethodName(nodeId) {
|
|
|
78
185
|
function inStructName(compName) {
|
|
79
186
|
return `InNR${pascal(compName)}`;
|
|
80
187
|
}
|
|
188
|
+
// ── strict de-box: the probe/wire seam + generated decode orchestration ───────────────────────
|
|
189
|
+
//
|
|
190
|
+
// A covered record read returns the consumer's WIRE (a value implementing WireRow), and the generated
|
|
191
|
+
// decode probes it per field from the declared type. The consumer supplies variant classification (a
|
|
192
|
+
// probe reports the producer's own wire tag as a free string); the generated code owns strictness —
|
|
193
|
+
// required/optional, present/absent, error assembly, fail-closed. The seam stays native: Probe/NumProbe
|
|
194
|
+
// are concrete enums and WireRow/WireList are monomorphized via mutual associated types (Row/List), so no
|
|
195
|
+
// boxed Value / dyn / Box crosses the boundary.
|
|
196
|
+
/** emitRustProbeWireTypes — the once-per-module probe enums + WireValue/WireRow/WireList seam traits.
|
|
197
|
+
* Concrete enums; monomorphized traits (WireValue's Row associated type threads WireRow, whose List
|
|
198
|
+
* threads WireList — nesting stays concrete, no dyn/Box). */
|
|
199
|
+
function emitRustProbeWireTypes() {
|
|
200
|
+
return `// Probe<T> / NumProbe — the outcome of classifying one wire attribute against a declared type. Got carries
|
|
201
|
+
// the matched value (a NumProbe's raw numeric text, which the de-box parses + range-checks so overflow is
|
|
202
|
+
// BC's to detect); actual_wire_type is the producer's own wire tag (a free string, e.g. a DynamoDB
|
|
203
|
+
// "S"/"N"/"BOOL"); raw_value is the offending value stringified. Concrete enums — no boxed Value.
|
|
204
|
+
pub enum Probe<T> {
|
|
205
|
+
Got(T),
|
|
206
|
+
Wrong { actual_wire_type: String, raw_value: String },
|
|
207
|
+
Null { actual_wire_type: String, raw_value: String },
|
|
208
|
+
Absent,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
pub enum NumProbe {
|
|
212
|
+
Got { raw: String, actual_wire_type: String },
|
|
213
|
+
Wrong { actual_wire_type: String, raw_value: String },
|
|
214
|
+
Null { actual_wire_type: String, raw_value: String },
|
|
215
|
+
Absent,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// WireValue — a node result's own wire datum (the uniform handler return): the producer's value at a
|
|
219
|
+
// result-consumption point. The generated de-box classifies it against the STATICALLY declared node type
|
|
220
|
+
// via these inherent methods (as_row / as_list yield the WireRow / WireList the nested decode then probes).
|
|
221
|
+
// A top value is always present, so there is no Absent — a producer-null is Null. Monomorphized (Row
|
|
222
|
+
// associated type threads WireRow) — no dyn, no Box, no boxed value crosses the seam.
|
|
223
|
+
pub trait WireValue: Sized {
|
|
224
|
+
type Row: WireRow;
|
|
225
|
+
fn as_string(&self) -> Probe<String>;
|
|
226
|
+
fn as_number(&self) -> NumProbe;
|
|
227
|
+
fn as_bool(&self) -> Probe<bool>;
|
|
228
|
+
fn as_row(&self) -> Probe<Self::Row>;
|
|
229
|
+
fn as_list(&self) -> Probe<<Self::Row as WireRow>::List>;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// WireRow / WireList — the consumer's wire item / wire array, probed per declared field / element by the
|
|
233
|
+
// generated de-box. The consumer implements them over its wire payload (a DynamoDB AttributeValue map)
|
|
234
|
+
// and classifies each attribute's variant; the de-box owns strictness. Monomorphized (Row/List mutual
|
|
235
|
+
// associated types) — no dyn, no Box, no boxed Value crosses the seam.
|
|
236
|
+
pub trait WireRow: Sized {
|
|
237
|
+
type List: WireList<Row = Self>;
|
|
238
|
+
// the attribute keys present (the entries of a wire map / DynamoDB "M") — iterated to decode a map(V).
|
|
239
|
+
fn keys(&self) -> Vec<String>;
|
|
240
|
+
fn probe_string(&self, field: &str) -> Probe<String>;
|
|
241
|
+
fn probe_number(&self, field: &str) -> NumProbe;
|
|
242
|
+
fn probe_bool(&self, field: &str) -> Probe<bool>;
|
|
243
|
+
fn probe_row(&self, field: &str) -> Probe<Self>;
|
|
244
|
+
fn probe_list(&self, field: &str) -> Probe<Self::List>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
pub trait WireList: Sized {
|
|
248
|
+
type Row: WireRow<List = Self>;
|
|
249
|
+
fn len(&self) -> usize;
|
|
250
|
+
fn is_empty(&self) -> bool {
|
|
251
|
+
self.len() == 0
|
|
252
|
+
}
|
|
253
|
+
fn elem_string(&self, i: usize) -> Probe<String>;
|
|
254
|
+
fn elem_number(&self, i: usize) -> NumProbe;
|
|
255
|
+
fn elem_bool(&self, i: usize) -> Probe<bool>;
|
|
256
|
+
fn elem_row(&self, i: usize) -> Probe<Self::Row>;
|
|
257
|
+
fn elem_list(&self, i: usize) -> Probe<Self>;
|
|
258
|
+
}`;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* renderRustDeBox — the ONE inline, recursive strict de-box renderer. Folds a language-neutral
|
|
262
|
+
* {@link DeBoxPlan} (the SSoT walk) into a single Rust EXPRESSION that evaluates to the decoded native
|
|
263
|
+
* value of the plan's type, obtained by probing `srcExpr` per the node's access (top → `as_*`, field →
|
|
264
|
+
* `probe_*(key)`, elem → `elem_*(i)`, mapval → `probe_*(&k)`). The recursion runs HERE at generation
|
|
265
|
+
* time (walking the plan), so the emitted Rust is FLAT, CONCRETE, fully-unrolled code: NO `decode_wire_*`
|
|
266
|
+
* helper function, NO output-side recursion — a nested record expands to a nested struct literal, a
|
|
267
|
+
* nested collection to a nested loop. A required-position mismatch does `return Err(de_*(...))` (fail
|
|
268
|
+
* closed, propagating the structured Error Value from the enclosing runner); an optional position
|
|
269
|
+
* (optDepth>0) maps absent/null to the all-None value. `ctr` hands out unique temp names (`sub2`, `l3`,
|
|
270
|
+
* `m4`, `i3`, `k4`) so nested blocks never collide. `idxExpr` is the element index / map-key argument the
|
|
271
|
+
* enclosing list / map loop supplies (empty for top / field). Mirrors the interpreter's outType check +
|
|
272
|
+
* the (now-deleted) per-position decoders, but TOTAL over the grammar (opt(named)/arr(arr)/map(opt)…) and
|
|
273
|
+
* inlined — the type is baked all the way through by the TS compiler, not the target monomorphizer.
|
|
274
|
+
*/
|
|
275
|
+
function renderRustDeBox(plan, srcExpr, ctr, indent, fail, idxExpr = "") {
|
|
276
|
+
const I = indent;
|
|
277
|
+
const I1 = indent + " ";
|
|
278
|
+
const id = ctr.n++;
|
|
279
|
+
const ml = rustStrLit(plan.model);
|
|
280
|
+
const fl = rustStrLit(plan.field);
|
|
281
|
+
const nl = rustStrLit(plan.expectedType);
|
|
282
|
+
const opt = plan.optDepth > 0;
|
|
283
|
+
const mismatch = fail(`de_type_mismatch(${ml}, ${fl}, ${nl}, actual_wire_type, raw_value)`);
|
|
284
|
+
const missing = fail(`de_missing_field(${ml}, ${fl}, ${nl})`);
|
|
285
|
+
// the probe expression for this node's access + a value-kind suffix (string/number/bool/row/list).
|
|
286
|
+
const probe = (suffix) => {
|
|
287
|
+
switch (plan.access.from) {
|
|
288
|
+
case "top":
|
|
289
|
+
return `${srcExpr}.as_${suffix}()`;
|
|
290
|
+
case "field":
|
|
291
|
+
return `${srcExpr}.probe_${suffix}(${rustStrLit(plan.access.key)})`;
|
|
292
|
+
case "elem":
|
|
293
|
+
return `${srcExpr}.elem_${suffix}(${idxExpr})`;
|
|
294
|
+
case "mapval":
|
|
295
|
+
return `${srcExpr}.probe_${suffix}(${idxExpr})`;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
// wrap the decoded value in optDepth `Some(...)` layers (present ⇒ Some^N(v); absent/null ⇒ None).
|
|
299
|
+
const wrapSome = (expr) => {
|
|
300
|
+
let e = expr;
|
|
301
|
+
for (let i = 0; i < plan.optDepth; i++)
|
|
302
|
+
e = `Some(${e})`;
|
|
303
|
+
return e;
|
|
304
|
+
};
|
|
305
|
+
// the three non-Got Probe<T> arms (scalar string/bool, record, list, map): required ⇒ wrong/null
|
|
306
|
+
// mismatch + absent missing; optional ⇒ wrong mismatch, null/absent None.
|
|
307
|
+
const probeArms = opt
|
|
308
|
+
? `${I1}Probe::Wrong { actual_wire_type, raw_value } => ${mismatch},\n${I1}Probe::Null { .. } | Probe::Absent => None,`
|
|
309
|
+
: `${I1}Probe::Wrong { actual_wire_type, raw_value }\n${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},\n${I1}Probe::Absent => ${missing},`;
|
|
310
|
+
if (plan.k === "scalar") {
|
|
311
|
+
if (plan.scalar === "int" || plan.scalar === "float") {
|
|
312
|
+
const rustT = plan.scalar === "int" ? "i64" : "f64";
|
|
313
|
+
const numArms = opt
|
|
314
|
+
? `${I1}NumProbe::Wrong { actual_wire_type, raw_value } => ${mismatch},\n${I1}NumProbe::Null { .. } | NumProbe::Absent => None,`
|
|
315
|
+
: `${I1}NumProbe::Wrong { actual_wire_type, raw_value }\n${I1}| NumProbe::Null { actual_wire_type, raw_value } => ${mismatch},\n${I1}NumProbe::Absent => ${missing},`;
|
|
316
|
+
return `match ${probe("number")} {
|
|
317
|
+
${I1}NumProbe::Got { raw, actual_wire_type } => match raw.parse::<${rustT}>() {
|
|
318
|
+
${I1} Ok(n) => ${wrapSome("n")},
|
|
319
|
+
${I1} Err(_) => ${fail(`de_overflow(${ml}, ${fl}, ${nl}, actual_wire_type, raw)`)},
|
|
320
|
+
${I1}},
|
|
321
|
+
${numArms}
|
|
322
|
+
${I}}`;
|
|
323
|
+
}
|
|
324
|
+
const meth = plan.scalar === "string" ? "string" : "bool";
|
|
325
|
+
return `match ${probe(meth)} {
|
|
326
|
+
${I1}Probe::Got(v) => ${wrapSome("v")},
|
|
327
|
+
${probeArms}
|
|
328
|
+
${I}}`;
|
|
329
|
+
}
|
|
330
|
+
if (plan.k === "record") {
|
|
331
|
+
const fieldInits = plan.fields
|
|
332
|
+
.map((f) => `${I1} ${rustFieldName(f.name)}: ${renderRustDeBox(f.plan, `sub${id}`, ctr, `${I1} `, fail)},`)
|
|
333
|
+
.join("\n");
|
|
334
|
+
return `match ${probe("row")} {
|
|
335
|
+
${I1}Probe::Got(sub${id}) => ${wrapSome(`${plan.typeName} {
|
|
336
|
+
${fieldInits}
|
|
337
|
+
${I1}}`)},
|
|
338
|
+
${probeArms}
|
|
339
|
+
${I}}`;
|
|
340
|
+
}
|
|
341
|
+
if (plan.k === "list") {
|
|
342
|
+
const elemExpr = renderRustDeBox(plan.elem, `l${id}`, ctr, `${I1} `, fail, `i${id}`);
|
|
343
|
+
return `match ${probe("list")} {
|
|
344
|
+
${I1}Probe::Got(l${id}) => ${wrapSome(`{
|
|
345
|
+
${I1} let mut acc${id} = Vec::with_capacity(l${id}.len());
|
|
346
|
+
${I1} for i${id} in 0..l${id}.len() {
|
|
347
|
+
${I1} acc${id}.push(${elemExpr});
|
|
348
|
+
${I1} }
|
|
349
|
+
${I1} acc${id}
|
|
350
|
+
${I1}}`)},
|
|
351
|
+
${probeArms}
|
|
352
|
+
${I}}`;
|
|
353
|
+
}
|
|
354
|
+
// map(V): the wire value is a producer map (a DynamoDB "M"); enumerate its keys, decode each value as V.
|
|
355
|
+
const valExpr = renderRustDeBox(plan.value, `m${id}`, ctr, `${I1} `, fail, `&k${id}`);
|
|
356
|
+
return `match ${probe("row")} {
|
|
357
|
+
${I1}Probe::Got(m${id}) => ${wrapSome(`{
|
|
358
|
+
${I1} let mut acc${id} = std::collections::BTreeMap::new();
|
|
359
|
+
${I1} for k${id} in m${id}.keys() {
|
|
360
|
+
${I1} let v${id} = ${valExpr};
|
|
361
|
+
${I1} acc${id}.insert(k${id}, v${id});
|
|
362
|
+
${I1} }
|
|
363
|
+
${I1} acc${id}
|
|
364
|
+
${I1}}`)},
|
|
365
|
+
${probeArms}
|
|
366
|
+
${I}}`;
|
|
367
|
+
}
|
|
368
|
+
/** returnErrFail — the default de-box mismatch control flow: propagate the structured Error Value out of
|
|
369
|
+
* the enclosing runner (fail/retry componentRef results + map/fanout element de-box, which fails closed
|
|
370
|
+
* regardless of elementPolicy — matching the interpreter's whole-node outType throw). */
|
|
371
|
+
const returnErrFail = (errValueExpr) => `return Err(${errValueExpr})`;
|
|
372
|
+
/** renderResultDeBox — inline strict de-box of a whole result WIRE (`srcExpr`, a WireValue) into its
|
|
373
|
+
* native value, from the TOTAL SSoT plan for `ref` at the top position. `fail` selects the mismatch
|
|
374
|
+
* control flow. This is the ONE de-box call the runner arms share (componentRef result + map/fanout
|
|
375
|
+
* element) — no per-position predicate, no helper decode fn. */
|
|
376
|
+
function renderResultDeBox(ref, srcExpr, nodeId, plan, indent, fail = returnErrFail) {
|
|
377
|
+
return renderRustDeBox(buildDeBoxPlan(ref, { from: "top" }, { model: nodeId, field: nodeId, plan }), srcExpr, { n: 0 }, indent, fail);
|
|
378
|
+
}
|
|
379
|
+
/** mapFanoutElemRef — the element ROW TypeRef a map/fanout node decodes (the per-element record). */
|
|
380
|
+
function mapFanoutElemRef(node, typedNodes, plan) {
|
|
381
|
+
const ref = typedNodes.get(node.id);
|
|
382
|
+
if ("fanout" in node)
|
|
383
|
+
return fanoutItemsElemRef(node, ref, plan);
|
|
384
|
+
return mapElemRowRef(node, ref, plan);
|
|
385
|
+
}
|
|
81
386
|
function staticArrElems(node) {
|
|
82
387
|
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
83
388
|
return null;
|
|
@@ -88,11 +393,60 @@ function staticArrElems(node) {
|
|
|
88
393
|
return Array.isArray(arg) ? arg : null;
|
|
89
394
|
}
|
|
90
395
|
/**
|
|
91
|
-
* portFieldRustType — the NATIVE Rust type for a covered port field (bc#90 / runtime-free)
|
|
92
|
-
*
|
|
93
|
-
|
|
94
|
-
*
|
|
95
|
-
*
|
|
396
|
+
* portFieldRustType — the NATIVE Rust type for a covered port field (bc#90 / runtime-free). The covered
|
|
397
|
+
* ports struct carries NO `Value` field: every port lowers to a concrete Rust type.
|
|
398
|
+
/** rustOwn — the ownership renderer native-expr applies at a resolved ref LEAF: an array field is cloned
|
|
399
|
+
* (owned) when `ownArrays` (a port) or borrowed (a cond `len`); a Copy scalar behind a guard is deref'd
|
|
400
|
+
* (unless a field was already accessed by value); anything else clones. */
|
|
401
|
+
function rustOwn(guard, ownArrays) {
|
|
402
|
+
return (expr, ref, fieldAccessed) => {
|
|
403
|
+
if (ref.kind === "arr")
|
|
404
|
+
return ownArrays ? `${expr}.clone()` : expr;
|
|
405
|
+
if (typeRefIsCopy(ref))
|
|
406
|
+
return guard && !fieldAccessed ? `*${expr}` : expr;
|
|
407
|
+
return `${expr}.clone()`;
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
/** nativeResolveHead — the ONE ref-head resolver the runtime-free native-expr compiler reads (cond `if`/
|
|
411
|
+
* branches, a map/fanout `when` guard, AND a port). A head is a `$as` element binding, a prior body node's
|
|
412
|
+
* typed cell (behind a RefCell borrow guard), or an input port (a by-value struct field). #139: the input
|
|
413
|
+
* ref comes from the inputPortTypeRef SSoT so an OPTIONAL port resolves as `{opt:…}` (native Option<T>). */
|
|
414
|
+
function nativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
|
|
415
|
+
const ownArrays = opts?.ownArrays === true;
|
|
416
|
+
return (head) => {
|
|
417
|
+
if (opts?.asBinding !== undefined && opts.asBase !== undefined && head === opts.asBinding.name) {
|
|
418
|
+
return { expr: opts.asBase, ref: opts.asBinding.ref, own: rustOwn(true, ownArrays) };
|
|
419
|
+
}
|
|
420
|
+
if (isPrior(head)) {
|
|
421
|
+
const ref = typedNodes.get(head);
|
|
422
|
+
return { expr: `${typedCell(head)}.borrow()`, ref, own: rustOwn(true, ownArrays) };
|
|
423
|
+
}
|
|
424
|
+
const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
|
|
425
|
+
if (ref === undefined)
|
|
426
|
+
return null;
|
|
427
|
+
return { expr: `in_.${rustFieldName(head)}`, ref, own: rustOwn(false, ownArrays) };
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/** compilePortNode — lower a port expression to its native ports-struct field via the runtime-free
|
|
431
|
+
* native-expr compiler: the ONE port lowering. The port's field TYPE is the compiled `ref`, its
|
|
432
|
+
* initializer the compiled `expr` — derived from a SINGLE lowering, so they cannot disagree. A port that
|
|
433
|
+
* cannot lower FAILS CLOSED; a FALLIBLE lowering (hoists statements) is returned as-is (only emitPortInit
|
|
434
|
+
* rejects it). Reuses the same nativeResolveHead cond/guard/port share, with `ownArrays` (a port owns). */
|
|
435
|
+
function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
|
|
436
|
+
const resolveHead = nativeResolveHead(comp, plan, typedNodes, isPrior, { ...opts, ownArrays: true });
|
|
437
|
+
try {
|
|
438
|
+
return new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compilePort(node);
|
|
439
|
+
}
|
|
440
|
+
catch (e) {
|
|
441
|
+
if (!(e instanceof GeneratorFailure))
|
|
442
|
+
throw e;
|
|
443
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} ${JSON.stringify(node)} does not lower to a native Rust value: ${e.message}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* portFieldRustType — the NATIVE Rust type for a covered port field, read off the ONE port lowering
|
|
448
|
+
* (compilePortNode / native-expr). Field type = the compiled TypeRef, so it can never disagree with the
|
|
449
|
+
* initializer (emitPortInit), which reads the SAME lowering for its value. `where` names the site.
|
|
96
450
|
*/
|
|
97
451
|
function portFieldRustType(node, comp, plan, typedNodes, where = "port", opts) {
|
|
98
452
|
const c = compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), where, opts);
|
|
@@ -105,12 +459,17 @@ function portFieldRustType(node, comp, plan, typedNodes, where = "port", opts) {
|
|
|
105
459
|
* reads `ports.f_<field>` DIRECTLY. The struct derives Clone (a bc#87 parallel stage clones it into the
|
|
106
460
|
* per-worker dispatch). Every field is a NATIVE Rust type (bc#90 / runtime-free) — NO boxed `Value`.
|
|
107
461
|
*/
|
|
108
|
-
function emitPortsStruct(comp, node, plan, typedNodes
|
|
462
|
+
function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
|
|
109
463
|
const name = portsStructName(comp.name, node.id);
|
|
110
464
|
const portNames = Object.keys(node.ports);
|
|
111
465
|
if (portNames.length === 0) {
|
|
112
466
|
return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
|
|
113
467
|
}
|
|
468
|
+
// a node WITH ports always resolves through the one port lowering, which needs the type plan + the
|
|
469
|
+
// prior-node map (both are supplied by every caller — narrow the optional params for the fields below).
|
|
470
|
+
if (plan === undefined || typedNodes === undefined) {
|
|
471
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native: ports struct for node '${node.id}' needs a type plan + prior-node map`);
|
|
472
|
+
}
|
|
114
473
|
const types = portNames.map((p) => portFieldRustType(node.ports[p], comp, plan, typedNodes, `port '${p}' of node '${node.id}'`, asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined));
|
|
115
474
|
const fields = portNames
|
|
116
475
|
.map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
|
|
@@ -135,7 +494,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
|
|
|
135
494
|
// #108: element ports may read the `$as` binding — resolve the over element type for native typing.
|
|
136
495
|
const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
137
496
|
const asBinding = { name: m.as, ref: elemRef, plan };
|
|
138
|
-
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, plan, typedNodes
|
|
497
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan, typedNodes);
|
|
139
498
|
return `${elemStruct}
|
|
140
499
|
|
|
141
500
|
// ${batch} — CONCRETE batched ports for map '${node.id}': the Vec of per-element CONCRETE ports structs
|
|
@@ -153,7 +512,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
|
|
|
153
512
|
const batch = `${name}Batch`;
|
|
154
513
|
const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
|
|
155
514
|
const asBinding = { name: f.as, ref: elemRef, plan };
|
|
156
|
-
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, plan, typedNodes
|
|
515
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan, typedNodes);
|
|
157
516
|
return `${elemStruct}
|
|
158
517
|
|
|
159
518
|
// ${batch} — CONCRETE batched ports for fanout '${node.id}': the Vec of per-id CONCRETE ports structs.
|
|
@@ -173,6 +532,20 @@ let rustExprTempSeq = 0;
|
|
|
173
532
|
/** module-level accumulator: set when the native-expr compiler emits a fallible helper call (so emit()
|
|
174
533
|
* bakes the helper library). Reset at the start of each emit(). */
|
|
175
534
|
const rustExprUsed = { helpers: false };
|
|
535
|
+
/** rustBlockBody — the body of a block-expression that runs `stmts` then yields `expr`. A trailing
|
|
536
|
+
* `let t = E; t` collapses to `E` (clippy `let_and_return`). */
|
|
537
|
+
function rustBlockBody(stmts, expr) {
|
|
538
|
+
const last = stmts[stmts.length - 1];
|
|
539
|
+
const m = last !== undefined ? /^let\s+([A-Za-z0-9_]+)(?::\s*[^=]+)?\s*=\s*(.+);$/.exec(last) : null;
|
|
540
|
+
if (m !== null && m[1] === expr)
|
|
541
|
+
return [...stmts.slice(0, -1), m[2]].join(" ");
|
|
542
|
+
return [...stmts, rustStripOuterParens(expr)].join(" ");
|
|
543
|
+
}
|
|
544
|
+
/** Drop a TRAILING `.clone()` — for a position that only reads the value (a presence test), so an owning
|
|
545
|
+
* clone added by a head resolver is pure waste. Only a clone at the very END is dropped. */
|
|
546
|
+
function rustStripTrailingClone(expr) {
|
|
547
|
+
return expr.endsWith(".clone()") ? expr.slice(0, -".clone()".length) : expr;
|
|
548
|
+
}
|
|
176
549
|
/** makeRustExprBackend — renders native-expr for Rust. `resolveHead` maps a ref head to an OWNED typed
|
|
177
550
|
* base expr (a prior-node cell clone / an input field / a $as element). Sets rustExprUsed.helpers when
|
|
178
551
|
* a fallible helper is emitted. The runner is Result<_, BehaviorError>, so hoistFallible uses `?`. */
|
|
@@ -216,47 +589,39 @@ function makeRustExprBackend(resolveHead, plan) {
|
|
|
216
589
|
notOp: (a) => `(!${a})`,
|
|
217
590
|
structLit: (name, inits) => `${name} { ${inits.map((i) => `${rustFieldName(i.field)}: ${i.expr}`).join(", ")} }`,
|
|
218
591
|
arrLit: (elemTy, items) => `{ let __v: Vec<${elemTy}> = vec![${items.join(", ")}]; __v }`,
|
|
219
|
-
// a projection port of static string literals —
|
|
220
|
-
//
|
|
592
|
+
// a projection port of static string literals — `vec!["a", "b"]` infers `&'static str` (the field is
|
|
593
|
+
// Vec<&'static str>), ZERO heap allocation.
|
|
221
594
|
staticStrArr: (literals) => ({ expr: `vec![${literals.map((s) => rustStrLit(s)).join(", ")}]`, type: "Vec<&'static str>" }),
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
optNone: (innerTy) => `Option::<${innerTy}>::None`,
|
|
225
|
-
// opt (`Option<T>`) null-presence test: present = `expr.is_some()`, absent = `expr.is_none()`.
|
|
226
|
-
// A presence test only READS the discriminant, so an owning clone of the operand (which a head
|
|
227
|
-
// resolver adds so the value can be moved into a port) is dropped here — testing `Option<String>`
|
|
228
|
-
// must not heap-allocate. Mirrors the go twin's zero-cost `!= nil`.
|
|
229
|
-
optIsSome: (expr) => `${rustStripTrailingClone(expr)}.is_some()`,
|
|
230
|
-
optIsNone: (expr) => `${rustStripTrailingClone(expr)}.is_none()`,
|
|
231
|
-
// opt (`Option<T>`) defaulted to a PURE default — it cannot fail and has no effect, so its
|
|
232
|
-
// evaluation order is UNOBSERVABLE and both forms below are correct (a FALLIBLE default, where
|
|
233
|
-
// laziness IS observable, goes to optCoalesceGuard instead). `unwrap_or` is EAGER — fine for a plain
|
|
234
|
-
// literal/copy. A default that allocates (an owned `String`, a struct/vec literal) uses
|
|
235
|
-
// `unwrap_or_else`, whose closure defers the allocation to the absent branch: not a semantic
|
|
236
|
-
// requirement here, but pointless work otherwise — and what clippy `or_fun_call` asks for.
|
|
595
|
+
// opt (Option<T>) defaulted to a PURE default — eager unwrap_or for a plain copy/path, else the lazy
|
|
596
|
+
// unwrap_or_else closure (defers an allocating default to the absent branch; clippy or_fun_call).
|
|
237
597
|
optUnwrapOr: (optExpr, defaultExpr, _innerTy) => /^[A-Za-z0-9_.:]+$/.test(defaultExpr) ? `${optExpr}.unwrap_or(${defaultExpr})` : `${optExpr}.unwrap_or_else(|| ${defaultExpr})`,
|
|
238
598
|
// opt defaulted to a FALLIBLE default — a `match`, NOT a closure: the default's hoisted `?` early
|
|
239
|
-
// returns must propagate out of the ENCLOSING fn
|
|
240
|
-
// (it would try to return from the closure). Only the None arm evaluates the default.
|
|
599
|
+
// returns must propagate out of the ENCLOSING fn. Only the None arm evaluates the default.
|
|
241
600
|
optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
|
|
242
601
|
`let ${temp}: ${ty} = match ${optExpr} {`,
|
|
243
602
|
` Some(${bindTemp}) => ${bindTemp},`,
|
|
244
603
|
` None => { ${rustBlockBody(dStmts, dExpr)} }`,
|
|
245
604
|
`};`,
|
|
246
605
|
],
|
|
606
|
+
// an opt (`Option<T>`) NONE literal. Annotate as `Option::<T>::None` so it type-infers in ANY
|
|
607
|
+
// position (a bare `None` can be ambiguous; the explicit turbofish keeps it a fully-typed value).
|
|
608
|
+
optNone: (innerTy) => `Option::<${innerTy}>::None`,
|
|
609
|
+
// opt (`Option<T>`) null-presence test: present = `expr.is_some()`, absent = `expr.is_none()`.
|
|
610
|
+
optIsSome: (expr) => `${rustStripTrailingClone(expr)}.is_some()`,
|
|
611
|
+
optIsNone: (expr) => `${rustStripTrailingClone(expr)}.is_none()`,
|
|
247
612
|
ternary: (cond, t, e, _ty) => `(if ${rustStripOuterParens(cond)} { ${rustStripOuterParens(t)} } else { ${rustStripOuterParens(e)} })`,
|
|
248
613
|
shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
|
|
249
614
|
// and: let temp = if left { <rStmts> rExpr } else { false }; or: if left { true } else { <rStmts> rExpr }.
|
|
250
615
|
// build a block that runs rStmts then yields rExpr (parens stripped — block-tail is not a sub-expr).
|
|
251
|
-
const rblock = `{ ${
|
|
616
|
+
const rblock = `{ ${rStmts.join(" ")} ${rustStripOuterParens(rExpr)} }`;
|
|
252
617
|
if (op === "and") {
|
|
253
618
|
return [`let ${temp}: bool = if ${rustStripOuterParens(left)} ${rblock} else { false };`];
|
|
254
619
|
}
|
|
255
620
|
return [`let ${temp}: bool = if ${rustStripOuterParens(left)} { true } else ${rblock};`];
|
|
256
621
|
},
|
|
257
622
|
condGuard: (temp, ty, cond, tStmts, tExpr, eStmts, eExpr) => {
|
|
258
|
-
const tblock = `{ ${
|
|
259
|
-
const eblock = `{ ${
|
|
623
|
+
const tblock = `{ ${tStmts.join(" ")} ${rustStripOuterParens(tExpr)} }`;
|
|
624
|
+
const eblock = `{ ${eStmts.join(" ")} ${rustStripOuterParens(eExpr)} }`;
|
|
260
625
|
return [`let ${temp}: ${ty} = if ${rustStripOuterParens(cond)} ${tblock} else ${eblock};`];
|
|
261
626
|
},
|
|
262
627
|
};
|
|
@@ -265,17 +630,6 @@ function makeRustExprBackend(resolveHead, plan) {
|
|
|
265
630
|
function rustSnake(name) {
|
|
266
631
|
return name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").replace(/([0-9])([a-z])/g, "$1_$2").toLowerCase();
|
|
267
632
|
}
|
|
268
|
-
/** rustBlockBody — the body of a block-expression that runs `stmts` then yields `expr`. A trailing
|
|
269
|
-
* `let t = E; t` is collapsed to `E`: binding a temp only to return it on the next line is clippy's
|
|
270
|
-
* `let_and_return` (an error under `-D warnings`), and the hoisted-fallible shape produces exactly that
|
|
271
|
-
* whenever the block's tail IS the hoisted temp. */
|
|
272
|
-
function rustBlockBody(stmts, expr) {
|
|
273
|
-
const last = stmts[stmts.length - 1];
|
|
274
|
-
const m = last !== undefined ? /^let\s+([A-Za-z0-9_]+)(?::\s*[^=]+)?\s*=\s*(.+);$/.exec(last) : null;
|
|
275
|
-
if (m !== null && m[1] === expr)
|
|
276
|
-
return [...stmts.slice(0, -1), m[2]].join(" ");
|
|
277
|
-
return [...stmts, rustStripOuterParens(expr)].join(" ");
|
|
278
|
-
}
|
|
279
633
|
/** strip ONE redundant outer paren pair from an expr (clippy `if (cond)` unused_parens). Only when the
|
|
280
634
|
* whole string is a single balanced `( … )` — a `(a) && (b)` stays untouched. */
|
|
281
635
|
function rustStripOuterParens(expr) {
|
|
@@ -293,12 +647,6 @@ function rustStripOuterParens(expr) {
|
|
|
293
647
|
}
|
|
294
648
|
return expr.slice(1, -1);
|
|
295
649
|
}
|
|
296
|
-
/** Drop a TRAILING `.clone()` from an expression — for a position that only reads the value (no move),
|
|
297
|
-
* so an owning clone added by a head resolver is pure waste. Only a clone at the very END is dropped: a
|
|
298
|
-
* `.clone()` mid-path (`t_x.borrow().clone().field`) still owns the field access that follows it. */
|
|
299
|
-
function rustStripTrailingClone(expr) {
|
|
300
|
-
return expr.endsWith(".clone()") ? expr.slice(0, -".clone()".length) : expr;
|
|
301
|
-
}
|
|
302
650
|
/** Rust float literal (finite; integral gets `.0`). */
|
|
303
651
|
function rustFloatLitExpr(n) {
|
|
304
652
|
if (Number.isInteger(n))
|
|
@@ -389,73 +737,6 @@ fn bc_expr_str_gt(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) == std::cmp::
|
|
|
389
737
|
#[allow(dead_code)]
|
|
390
738
|
fn bc_expr_str_ge(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) != std::cmp::Ordering::Less }`;
|
|
391
739
|
}
|
|
392
|
-
// ── CONCRETE per-node row structs + row→outType copiers (mirror the Go concrete contract) ──────────
|
|
393
|
-
//
|
|
394
|
-
// Each covered node has a CONCRETE row struct `RawRowNR<Comp><Node>` whose fields are the node's projected
|
|
395
|
-
// outType fields (typed, flattened) plus a per-node error signal (is_error/err). A named-struct outType
|
|
396
|
-
// flattens to one Rust field per outType field; a scalar/arr/opt node carries a single `val: <typed>`
|
|
397
|
-
// payload. The per-component `HandlerNR<Comp>` trait has one method per node returning that concrete
|
|
398
|
-
// struct; the runner reads `row.<field>` DIRECTLY (no `RawValue`, no `match`, no `Value::`) and copies
|
|
399
|
-
// each field into the node's outType struct local.
|
|
400
|
-
/** emitRawRowStructs — emit the concrete `RawRowNR<Comp><Node>` struct per covered node (+ per-element
|
|
401
|
-
* `RawElemNR<Comp><Node>` for a covered map). Every row carries the per-node error signal (is_error/err). */
|
|
402
|
-
function emitRawRowStructs(comp, typedNodes, plan) {
|
|
403
|
-
const parts = [];
|
|
404
|
-
for (const n of comp.body) {
|
|
405
|
-
if ("cond" in n)
|
|
406
|
-
continue; // #108: a cond node has no handler-result row (pure Expression).
|
|
407
|
-
const ref = typedNodes.get(n.id);
|
|
408
|
-
if ("fanout" in n) {
|
|
409
|
-
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
410
|
-
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
411
|
-
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
|
|
412
|
-
`#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub is_error: bool,\n pub err: String,\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
if ("map" in n) {
|
|
416
|
-
const arrRef = ref;
|
|
417
|
-
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
418
|
-
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
419
|
-
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
|
|
420
|
-
`#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub is_error: bool,\n pub err: String,\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
|
|
424
|
-
}
|
|
425
|
-
return parts.join("\n\n");
|
|
426
|
-
}
|
|
427
|
-
/** Emit one concrete row struct: flatten a named outType to typed fields, else a single `val` payload;
|
|
428
|
-
* always carry is_error/err. Derives Default so a handler can build an error row cheaply (`..Default`). */
|
|
429
|
-
function emitOneRowStruct(name, ref, plan) {
|
|
430
|
-
const lines = [];
|
|
431
|
-
lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType; + error signal).`);
|
|
432
|
-
lines.push(`#[derive(Default)]`);
|
|
433
|
-
lines.push(`pub struct ${name} {`);
|
|
434
|
-
lines.push(` pub is_error: bool,`);
|
|
435
|
-
lines.push(` pub err: String,`);
|
|
436
|
-
if (ref.kind === "named") {
|
|
437
|
-
const decl = findDecl(plan, ref.name);
|
|
438
|
-
for (const f of decl.fields) {
|
|
439
|
-
lines.push(` pub ${rustFieldName(f.name)}: ${renderTypeRef(f.type)}, // ${JSON.stringify(f.name)}`);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
else {
|
|
443
|
-
lines.push(` pub val: ${renderTypeRef(ref)},`);
|
|
444
|
-
}
|
|
445
|
-
lines.push(`}`);
|
|
446
|
-
return lines.join("\n");
|
|
447
|
-
}
|
|
448
|
-
/** emitRowCopy — the statement copying a concrete row's typed fields into the node's outType local
|
|
449
|
-
* `target` (a value of Rust type renderTypeRef(ref)). Named: struct literal from the row's moved fields;
|
|
450
|
-
* scalar/arr/opt: `.val`. `rowExpr` is the concrete row VALUE (moved). No dynamic read — pure move/copy. */
|
|
451
|
-
function emitRowMove(target, rowExpr, ref, plan) {
|
|
452
|
-
if (ref.kind === "named") {
|
|
453
|
-
const decl = findDecl(plan, ref.name);
|
|
454
|
-
const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: ${rowExpr}.${rustFieldName(f.name)}`).join(", ");
|
|
455
|
-
return `${target} = ${ref.name} { ${inits} };`;
|
|
456
|
-
}
|
|
457
|
-
return `${target} = ${rowExpr}.val;`;
|
|
458
|
-
}
|
|
459
740
|
/**
|
|
460
741
|
* boundRustType (change #3 — typed `bound`) — the CONCRETE Rust type of a covered node's `bound` handler
|
|
461
742
|
* param (bc#90 / runtime-free). `bound` is the parent-produced key (the bindField value); it replaces the
|
|
@@ -503,10 +784,19 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
503
784
|
const fnKwFor = (nodeId) => (ap.nodeIsAsync(nodeId) ? "async fn" : "fn");
|
|
504
785
|
const lines = [];
|
|
505
786
|
lines.push(`// ${handlerTraitName(comp.name)} — the CONCRETE per-component handler seam: one typed method`);
|
|
506
|
-
lines.push(`// per covered node (native ports struct IN,
|
|
507
|
-
lines.push(`// /
|
|
508
|
-
lines.push(`//
|
|
787
|
+
lines.push(`// per covered node (native ports struct IN, WIRE value OUT). No generic boxed-ports / boxed-value`);
|
|
788
|
+
lines.push(`// / dynamic accessor crosses the covered boundary — the consumer implements each node_* by`);
|
|
789
|
+
lines.push(`// returning its own wire payload as a WireValue; the generated inline de-box turns that into the`);
|
|
790
|
+
lines.push(`// node's concrete native result (see INTEGRATION.md §6).`);
|
|
509
791
|
lines.push(`pub trait ${handlerTraitName(comp.name)} {`);
|
|
792
|
+
// every covered node returns the consumer's WIRE (a single associated `Wire: WireValue`) — the producer's
|
|
793
|
+
// datum at the node's result-consumption point; the generated inline de-box classifies it against the
|
|
794
|
+
// STATICALLY declared type. A batched map returns one wire per element (Vec); a fanout returns the aligned
|
|
795
|
+
// per-id wires (Vec<Option<Wire>> — None = a DANGLING id). The `+ Send` bound lets the wire cross the
|
|
796
|
+
// parallel-stage runner's scoped fan-out threads.
|
|
797
|
+
const hasHandler = comp.body.some((n) => !("cond" in n));
|
|
798
|
+
if (hasHandler)
|
|
799
|
+
lines.push(` type Wire: WireValue + Send;`);
|
|
510
800
|
for (const n of comp.body) {
|
|
511
801
|
if ("cond" in n)
|
|
512
802
|
continue; // #108: a cond node has no handler method (pure Expression).
|
|
@@ -514,21 +804,23 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
514
804
|
const fnKw = fnKwFor(n.id);
|
|
515
805
|
const bt = boundRustType(n, typedNodes, plan);
|
|
516
806
|
if ("fanout" in n) {
|
|
517
|
-
// fanout (v3): one batched dispatch of the deduped id list
|
|
807
|
+
// fanout (v3): one batched dispatch of the deduped id list, returning the aligned element WIRES the
|
|
808
|
+
// runner de-boxes then dedupe/drops. Each aligned position is Option<Self::Wire> — None = a DANGLING
|
|
809
|
+
// id (the runner maps it to the zero elem row).
|
|
518
810
|
const portsT = `${portsStructName(comp.name, n.id)}Batch`;
|
|
519
|
-
|
|
520
|
-
lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Option<${retT}>;`);
|
|
811
|
+
lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Result<Vec<Option<Self::Wire>>, BehaviorError>;`);
|
|
521
812
|
continue;
|
|
522
813
|
}
|
|
523
814
|
if ("map" in n) {
|
|
524
815
|
const m = n.map;
|
|
525
816
|
const batched = m.batched === true;
|
|
526
817
|
const portsT = batched ? `${portsStructName(comp.name, n.id)}Batch` : portsStructName(comp.name, n.id);
|
|
527
|
-
|
|
528
|
-
|
|
818
|
+
// a map returns the element WIRE(s): batched → Vec<Self::Wire>, non-batched → Self::Wire.
|
|
819
|
+
const retT = batched ? "Vec<Self::Wire>" : "Self::Wire";
|
|
820
|
+
lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Result<${retT}, BehaviorError>;`);
|
|
529
821
|
continue;
|
|
530
822
|
}
|
|
531
|
-
lines.push(` ${fnKw} ${method}(&self, ports: &${portsStructName(comp.name, n.id)}, bound: ${bt}) ->
|
|
823
|
+
lines.push(` ${fnKw} ${method}(&self, ports: &${portsStructName(comp.name, n.id)}, bound: ${bt}) -> Result<Self::Wire, BehaviorError>;`);
|
|
532
824
|
}
|
|
533
825
|
lines.push(`}`);
|
|
534
826
|
return lines.join("\n");
|
|
@@ -541,18 +833,16 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
541
833
|
function mapElemMaterializerName(compName, nodeId) {
|
|
542
834
|
return `materialize_map_elem_${sanitize(compName)}_${typedCell(nodeId)}`;
|
|
543
835
|
}
|
|
544
|
-
/** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
* unambiguous. Both converge to the SAME produced-array ref, so existing goldens stay byte-identical. */
|
|
836
|
+
/** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType`.
|
|
837
|
+
*
|
|
838
|
+
* A map node's `outType` is its ELEMENT type (scp-error.md "Map element type"), so the produced array
|
|
839
|
+
* is `{arr: element}`. This is the same reading the compiler applies when it resolves what a node
|
|
840
|
+
* result reference has (`type-gate`: `"map" in n ? {arr:ot} : ot`) — the emitter follows that SSoT
|
|
841
|
+
* rather than inspecting the annotation's shape, so an element that is ITSELF an `arr` stays
|
|
842
|
+
* expressible (element `arr(T)` produces `arr(arr(T))`). */
|
|
552
843
|
function mapNodeArrRef(node, plan) {
|
|
553
844
|
const outT = node.outType;
|
|
554
|
-
|
|
555
|
-
return deriveTypeRef((isArr ? outT : { arr: outT }), plan);
|
|
845
|
+
return deriveTypeRef({ arr: outT }, plan);
|
|
556
846
|
}
|
|
557
847
|
/** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
|
|
558
848
|
* type (into) or the element outType directly (no-into). This is the type of RawElemNR<Comp><Node>. */
|
|
@@ -572,9 +862,8 @@ function mapElemRowRef(node, arrRef, plan) {
|
|
|
572
862
|
}
|
|
573
863
|
/**
|
|
574
864
|
* emitMapElemMaterializers — for each covered map...into node, emit the AUGMENTED-element copier: copy
|
|
575
|
-
* the over element's typed fields +
|
|
576
|
-
*
|
|
577
|
-
* typed into row).
|
|
865
|
+
* the over element's typed fields + place the already-de-boxed `into` value into its field. Pure struct
|
|
866
|
+
* field assignment — NO dynamic value read (the inline de-box already produced the typed `into` value).
|
|
578
867
|
*/
|
|
579
868
|
function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
580
869
|
const parts = [];
|
|
@@ -596,7 +885,6 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
|
596
885
|
const overFields = decl.fields.filter((f) => f.name !== intoKey);
|
|
597
886
|
const overElemTy = mapOverElemType(comp, n, typedNodes, plan);
|
|
598
887
|
const fn = mapElemMaterializerName(comp.name, n.id);
|
|
599
|
-
const elemRowGo = rawElemStructName(comp.name, n.id);
|
|
600
888
|
const intoRowRef = mapElemRowRef(n, arrRef, plan);
|
|
601
889
|
const inits = [];
|
|
602
890
|
// Copy scalars (i64/f64/bool) copy directly; String/named/arr/opt clone (clippy clone_on_copy clean).
|
|
@@ -604,15 +892,12 @@ function emitMapElemMaterializers(comp, typedNodes, plan) {
|
|
|
604
892
|
const isCopy = f.type.kind === "scalar" && (f.type.scalar === "int" || f.type.scalar === "float" || f.type.scalar === "bool");
|
|
605
893
|
inits.push(` ${rustFieldName(f.name)}: over.${rustFieldName(f.name)}${isCopy ? "" : ".clone()"},`);
|
|
606
894
|
}
|
|
607
|
-
//
|
|
608
|
-
const intoValue = intoRowRef.kind === "named"
|
|
609
|
-
? `${intoRowRef.name} { ${findDecl(plan, intoRowRef.name).fields.map((f) => `${rustFieldName(f.name)}: into.${rustFieldName(f.name)}`).join(", ")} }`
|
|
610
|
-
: `into.val`;
|
|
895
|
+
// the `into` param is the already-de-boxed into value (its native type) — drop it straight in.
|
|
611
896
|
parts.push(`#[allow(dead_code)]
|
|
612
|
-
fn ${fn}(over: &${overElemTy}, into: ${
|
|
897
|
+
fn ${fn}(over: &${overElemTy}, into: ${renderTypeRef(intoRowRef)}) -> ${elemRef.name} {
|
|
613
898
|
${elemRef.name} {
|
|
614
899
|
${inits.join("\n")}
|
|
615
|
-
${rustFieldName(intoKey)}:
|
|
900
|
+
${rustFieldName(intoKey)}: into,
|
|
616
901
|
}
|
|
617
902
|
}`);
|
|
618
903
|
void intoField;
|
|
@@ -664,26 +949,42 @@ function reconstructR(e) {
|
|
|
664
949
|
return e.node;
|
|
665
950
|
}
|
|
666
951
|
}
|
|
667
|
-
/**
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
952
|
+
/**
|
|
953
|
+
* numLiteralRustExpr — a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
|
|
954
|
+
* `classifyExpr` leaves a bare number `dynamic` (its checked int/float range rules are the evaluate
|
|
955
|
+
* SSoT), so it never reaches the static str/bool/null branch — but a number literal IS statically
|
|
956
|
+
* resolvable to a native `Value`: the SAME range check the runtime applies (integral literal in the
|
|
957
|
+
* safe 2^53-1 window → Value::Int; a fractional/exponent literal → the checked Value::Float) is
|
|
958
|
+
* performed HERE at generation time. Returns the Rust expression for the literal's Value, or null
|
|
959
|
+
* when the node is not a bare number OR an integral literal is out of the safe window (keeping the
|
|
960
|
+
* runtime INVALID_LITERAL semantics — the port is not covered natively, the component falls through).
|
|
961
|
+
* Mirrors behavior_contracts eval of a bare number literal exactly.
|
|
962
|
+
*/
|
|
963
|
+
const NUM_SAFE_INT_R = 9007199254740991; // 2^53 - 1 (expr safeInt)
|
|
964
|
+
function numLiteralRustExpr(node) {
|
|
965
|
+
if (typeof node !== "number" || !Number.isFinite(node))
|
|
966
|
+
return null;
|
|
967
|
+
if (Number.isInteger(node)) {
|
|
968
|
+
if (node < -NUM_SAFE_INT_R || node > NUM_SAFE_INT_R)
|
|
969
|
+
return null; // out of safe window → not covered
|
|
970
|
+
return `Value::Int(${node}i64)`;
|
|
971
|
+
}
|
|
972
|
+
// fractional / exponent literal → checked f64 (finite, guaranteed above).
|
|
973
|
+
return `Value::Float(${node}f64)`;
|
|
974
|
+
}
|
|
975
|
+
/** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
|
|
976
|
+
* resolvable to a native Value with no Obj / no evaluate interpreter. */
|
|
977
|
+
/** planForComp — a TypePlan for a single component (its named types are component-scoped). */
|
|
978
|
+
function planForComp(comp) {
|
|
979
|
+
return buildTypePlan({ irVersion: 1, exprVersion: 2, components: [comp] });
|
|
980
|
+
}
|
|
981
|
+
/** buildTypedNodes — the prior-node typed-cell TypeRef map (control-gate nodes typeless; a map node
|
|
982
|
+
* carries its produced-array ref). An untyped node is omitted. */
|
|
681
983
|
function buildTypedNodes(comp, plan) {
|
|
682
984
|
const typedNodes = new Map();
|
|
683
985
|
for (const n of comp.body) {
|
|
684
986
|
if (isControlGate(n))
|
|
685
987
|
continue;
|
|
686
|
-
// coverage probes under-typed components too; an unresolvable node just isn't a prior-node cell.
|
|
687
988
|
try {
|
|
688
989
|
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
|
|
689
990
|
}
|
|
@@ -693,6 +994,26 @@ function buildTypedNodes(comp, plan) {
|
|
|
693
994
|
}
|
|
694
995
|
return typedNodes;
|
|
695
996
|
}
|
|
997
|
+
/** mapAsBinding — the `$as` element binding for a map node's element ports, or undefined (uncovered). */
|
|
998
|
+
function mapAsBinding(comp, node, typedNodes, plan) {
|
|
999
|
+
try {
|
|
1000
|
+
return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
|
|
1001
|
+
}
|
|
1002
|
+
catch {
|
|
1003
|
+
return undefined;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/** portIsStatic — the ELIGIBILITY gate ASKS the one port lowering (compilePortNode / native-expr) whether
|
|
1007
|
+
* it can lower this port. A port that does not lower (or lowers fallibly) fails closed. */
|
|
1008
|
+
function portIsStatic(node, comp, plan, typedNodes, asBinding) {
|
|
1009
|
+
try {
|
|
1010
|
+
compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), "port", asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined);
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
catch {
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
696
1017
|
/** The output must lower to a typed struct/value with no scope read: a ref to a BODY NODE, an
|
|
697
1018
|
* obj/arr of such, or a concat. A ref to an input param / non-concat operator is not lowerable. */
|
|
698
1019
|
function outputIsNativeLowerable(comp) {
|
|
@@ -724,7 +1045,7 @@ function outputIsNativeLowerable(comp) {
|
|
|
724
1045
|
/**
|
|
725
1046
|
* coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet), rust twin of the go predicate.
|
|
726
1047
|
*/
|
|
727
|
-
function coveredMapNode(n, bodyIds, comp
|
|
1048
|
+
function coveredMapNode(n, bodyIds, comp) {
|
|
728
1049
|
if (n === null || typeof n !== "object" || !("map" in n))
|
|
729
1050
|
return false;
|
|
730
1051
|
const m = n.map;
|
|
@@ -756,25 +1077,17 @@ function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
|
|
|
756
1077
|
return false;
|
|
757
1078
|
}
|
|
758
1079
|
const ports = m.ports;
|
|
1080
|
+
const plan = planForComp(comp);
|
|
1081
|
+
const typedNodes = buildTypedNodes(comp, plan);
|
|
759
1082
|
const asBinding = mapAsBinding(comp, n, typedNodes, plan);
|
|
760
1083
|
for (const p of Object.keys(ports))
|
|
761
1084
|
if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
|
|
762
1085
|
return false;
|
|
763
1086
|
return true;
|
|
764
1087
|
}
|
|
765
|
-
/** the `$as` element binding for a covered map/fanout node's element ports (name + over-element TypeRef),
|
|
766
|
-
* or undefined when the over element type does not resolve (the node is then uncovered upstream). */
|
|
767
|
-
function mapAsBinding(comp, node, typedNodes, plan) {
|
|
768
|
-
try {
|
|
769
|
-
return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
|
|
770
|
-
}
|
|
771
|
-
catch {
|
|
772
|
-
return undefined;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
1088
|
/** coveredFanoutNode (v3, rust twin) — a first-class fanout node is covered when its over is a covered
|
|
776
1089
|
* ref (prior-node arr / input array with elemType) and its element ports are static. See the go twin. */
|
|
777
|
-
function coveredFanoutNode(n, bodyIds, comp
|
|
1090
|
+
function coveredFanoutNode(n, bodyIds, comp) {
|
|
778
1091
|
if (n === null || typeof n !== "object" || !("fanout" in n))
|
|
779
1092
|
return false;
|
|
780
1093
|
const f = n.fanout;
|
|
@@ -794,6 +1107,8 @@ function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
|
|
|
794
1107
|
return false;
|
|
795
1108
|
}
|
|
796
1109
|
const ports = f.ports;
|
|
1110
|
+
const plan = planForComp(comp);
|
|
1111
|
+
const typedNodes = buildTypedNodes(comp, plan);
|
|
797
1112
|
let asBinding;
|
|
798
1113
|
try {
|
|
799
1114
|
asBinding = { name: f.as, ref: fanoutOverElemInfo(comp, n, typedNodes, plan).elemRef, plan };
|
|
@@ -838,8 +1153,8 @@ function fanoutOverElemInfo(comp, node, typedNodes, plan) {
|
|
|
838
1153
|
}
|
|
839
1154
|
/** A covered read SHAPE with static ports + native-lowerable output (+ covered map...into), OR a
|
|
840
1155
|
* real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point reads. */
|
|
841
|
-
function isSequentialComponentRefRead(comp
|
|
842
|
-
return coverageRejectReason(comp
|
|
1156
|
+
function isSequentialComponentRefRead(comp) {
|
|
1157
|
+
return coverageRejectReason(comp) === null;
|
|
843
1158
|
}
|
|
844
1159
|
/**
|
|
845
1160
|
* coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89), rust twin of
|
|
@@ -848,11 +1163,12 @@ function isSequentialComponentRefRead(comp, plan) {
|
|
|
848
1163
|
* IDENTICAL to isSequentialComponentRefRead (defined as `coverageRejectReason(c) === null`) so the two
|
|
849
1164
|
* can never disagree. This is the message the fail-closed dispatch surfaces per non-native component.
|
|
850
1165
|
*/
|
|
851
|
-
function coverageRejectReason(comp
|
|
1166
|
+
function coverageRejectReason(comp) {
|
|
1167
|
+
const crPlan = planForComp(comp);
|
|
1168
|
+
const crTypedNodes = buildTypedNodes(comp, crPlan);
|
|
852
1169
|
const ep = execPlan(comp);
|
|
853
1170
|
if (ep === null)
|
|
854
1171
|
return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
|
|
855
|
-
const typedNodes = buildTypedNodes(comp, plan);
|
|
856
1172
|
// #114: a bindField relation child (single|connection) IS covered inside a parallel stage — the
|
|
857
1173
|
// sibling hasMany fan-out (parent → members ∥ permissions). Its bound-key skip-gate is preflighted
|
|
858
1174
|
// per member (from the settled parent) BEFORE dispatch, so the scoped-thread fan-out keeps
|
|
@@ -890,12 +1206,12 @@ function coverageRejectReason(comp, plan) {
|
|
|
890
1206
|
continue;
|
|
891
1207
|
}
|
|
892
1208
|
if ("fanout" in n) {
|
|
893
|
-
if (!coveredFanoutNode(n, bodyIds, comp
|
|
1209
|
+
if (!coveredFanoutNode(n, bodyIds, comp))
|
|
894
1210
|
return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
|
|
895
1211
|
continue;
|
|
896
1212
|
}
|
|
897
1213
|
if ("map" in n) {
|
|
898
|
-
if (!coveredMapNode(n, bodyIds, comp
|
|
1214
|
+
if (!coveredMapNode(n, bodyIds, comp))
|
|
899
1215
|
return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
|
|
900
1216
|
continue;
|
|
901
1217
|
}
|
|
@@ -914,7 +1230,7 @@ function coverageRejectReason(comp, plan) {
|
|
|
914
1230
|
if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
915
1231
|
return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
|
|
916
1232
|
for (const p of Object.keys(cr.ports))
|
|
917
|
-
if (portIsStatic(cr.ports[p], comp,
|
|
1233
|
+
if (portIsStatic(cr.ports[p], comp, crPlan, crTypedNodes) === false)
|
|
918
1234
|
return `node '${n.id}' port '${p}' is not statically resolvable`;
|
|
919
1235
|
}
|
|
920
1236
|
if (!outputIsNativeLowerable(comp))
|
|
@@ -931,42 +1247,28 @@ function isFullyTyped(comp) {
|
|
|
931
1247
|
return false;
|
|
932
1248
|
return true;
|
|
933
1249
|
}
|
|
934
|
-
function isNativeRaw(comp
|
|
935
|
-
return isSequentialComponentRefRead(comp
|
|
1250
|
+
function isNativeRaw(comp) {
|
|
1251
|
+
return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
|
|
936
1252
|
}
|
|
937
|
-
function assertTypedCoverage(comp
|
|
938
|
-
if (!isSequentialComponentRefRead(comp
|
|
1253
|
+
function assertTypedCoverage(comp) {
|
|
1254
|
+
if (!isSequentialComponentRefRead(comp))
|
|
939
1255
|
return;
|
|
940
1256
|
if (isFullyTyped(comp))
|
|
941
1257
|
return;
|
|
942
1258
|
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).`);
|
|
943
1259
|
}
|
|
944
1260
|
// ── typed input struct (InNR<Comp>) + NATIVE scalar port lowering ────────────────────────────
|
|
945
|
-
/**
|
|
946
|
-
* inputPortRustType — the native Rust type for an input port's declared type. Scalars lower to the
|
|
947
|
-
* concrete scalar; `array`/`map` with a declared elemType to `Vec<ElemT>` / `BTreeMap<String, ElemT>`;
|
|
948
|
-
* an OPTIONAL port (`{opt:T}` → `required:false`) to `Option<T>` — the native representation of "the
|
|
949
|
-
* value is absent". A declared type with no native lowering stays the boxed `Value`.
|
|
950
|
-
*
|
|
951
|
-
* An OPTIONAL port whose inner type has no native lowering FAILS CLOSED: `Option<Value>` would be a
|
|
952
|
-
* boxed escape, and emitting the bare inner type would silently turn "absent" into a zero value. A port
|
|
953
|
-
* whose optionality cannot be represented is never silently coerced.
|
|
954
|
-
*/
|
|
1261
|
+
/** Rust type for an input port's declared type (scalar → concrete; array+elemType → Vec<ElemT>; else Value). */
|
|
955
1262
|
function inputPortRustType(schema, plan, where = "input port") {
|
|
956
1263
|
const ref = inputPortTypeRef(schema, plan);
|
|
957
1264
|
if (ref !== undefined)
|
|
958
1265
|
return renderTypeRef(ref);
|
|
959
1266
|
if (inputPortIsOptional(schema))
|
|
960
|
-
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
|
|
1267
|
+
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. Declare an elemType for an array/map, or use a natively-lowerable scalar.`);
|
|
961
1268
|
return "Value";
|
|
962
1269
|
}
|
|
963
|
-
/** The scalar kind
|
|
964
|
-
* An OPTIONAL port is deliberately NOT a scalar kind here: its native type is `Option<T>`, and the
|
|
965
|
-
* callers of this function build REQUIRED scalar slots (`in_.<field>` read straight into a non-opt port
|
|
966
|
-
* field). Answering "i64" for an `{opt:"int"}` port is exactly how "absent" would silently become `0`. */
|
|
1270
|
+
/** The scalar kind an input port declares, or undefined if it is not a native scalar. */
|
|
967
1271
|
function inputScalarKind(schema) {
|
|
968
|
-
if (inputPortIsOptional(schema))
|
|
969
|
-
return undefined;
|
|
970
1272
|
switch (schema?.type) {
|
|
971
1273
|
case "string":
|
|
972
1274
|
case "literal": // a `"literal"` union is a constrained String (see inputPortRustType).
|
|
@@ -990,6 +1292,8 @@ function emitInStruct(comp, plan) {
|
|
|
990
1292
|
if (ports.length === 0) {
|
|
991
1293
|
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\n#[derive(Default)]\npub struct ${name};`;
|
|
992
1294
|
}
|
|
1295
|
+
// #139: inputPortRustType reads the inputPortTypeRef SSoT — an OPTIONAL port is `Option<T>` (absent →
|
|
1296
|
+
// None); an optional whose inner type has no native lowering FAILS CLOSED (never a boxed escape).
|
|
993
1297
|
const fields = ports
|
|
994
1298
|
.map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`)}, // ${JSON.stringify(p)}`)
|
|
995
1299
|
.join("\n");
|
|
@@ -1000,18 +1304,30 @@ pub struct ${name} {
|
|
|
1000
1304
|
${fields}
|
|
1001
1305
|
}`;
|
|
1002
1306
|
}
|
|
1003
|
-
/**
|
|
1004
|
-
*
|
|
1307
|
+
/** A ref resolver for native lowering: given a ref head + remaining field path, return a native scalar
|
|
1308
|
+
* Rust expression (typed input field / prior-node typed struct scalar / element binding field), or null
|
|
1309
|
+
* when the ref does not resolve to a REQUIRED (non-opt) native scalar (then the caller falls back to the
|
|
1310
|
+
* Value path). Returned exprs are OWNED (String cloned / scalar copied) — usable in a `format!` / literal. */
|
|
1311
|
+
// bc#90 / runtime-free: the old `emitStaticR` / `inputLeafR` / `valueLeafR` port lowering (which built
|
|
1312
|
+
// `Value` leaves via the primitives::* helpers and struct-field serializers) is REMOVED — every covered
|
|
1313
|
+
// port now lowers to a FULLY NATIVE Rust value in emitPortInit (string / bool / static-string-array /
|
|
1314
|
+
// number literal), and a genuinely-dynamic port fails closed. There is no boxed-Value fallback on the
|
|
1315
|
+
// covered read plane.
|
|
1005
1316
|
/**
|
|
1006
|
-
* emitPortInit — the port field
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1317
|
+
* emitPortInit — emit the port field build for one port as a FULLY NATIVE Rust expression (bc#90 /
|
|
1318
|
+
* runtime-free). Every covered port lowers to a concrete Rust value assigned straight into the ports
|
|
1319
|
+
* struct field — NO `Value`, NO fallback path, NO `match`:
|
|
1320
|
+
* - a static string array (projection) → `vec!["a", "b", ...]` (change #2);
|
|
1321
|
+
* - a bare int/float number literal (e.g. Query `limit: 20`) → `20i64` / `<n>f64`;
|
|
1322
|
+
* - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
|
|
1323
|
+
* A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered plane —
|
|
1324
|
+
* a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
|
|
1325
|
+
* ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
|
|
1010
1326
|
*/
|
|
1011
1327
|
function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
|
|
1012
1328
|
const c = compilePortNode(expr, comp, plan, typedNodes, isPrior, `port '${pn}'`, opts);
|
|
1013
1329
|
if (c.stmts.length > 0)
|
|
1014
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): port '${pn}' ${JSON.stringify(expr)} lowers to a FALLIBLE expression (
|
|
1330
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): port '${pn}' ${JSON.stringify(expr)} lowers to a FALLIBLE expression (INT_OVERFLOW / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS) — statements with an early error-return, which a ports-struct literal cannot host. Use a non-fallible port expression.`);
|
|
1015
1331
|
return `${portFieldName(pn)}: ${c.expr}`;
|
|
1016
1332
|
}
|
|
1017
1333
|
// ── the combined runner (STRUCT-returning; INLINE sequential; native ports in; concrete row out) ──
|
|
@@ -1108,11 +1424,11 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1108
1424
|
const lines = [];
|
|
1109
1425
|
lines.push(`// run_native_raw_struct_${fn} — the STRUCT-RETURNING combined read (bc#77/#87/#94): the fully`);
|
|
1110
1426
|
lines.push(`// de-plumbed CONCRETE path. Generic over the per-component CONCRETE ${handlerTraitName(comp.name)} trait,`);
|
|
1111
|
-
lines.push(`// whose node_* methods take the node's native ports struct and return the
|
|
1112
|
-
lines.push(`//
|
|
1113
|
-
lines.push(`//
|
|
1114
|
-
lines.push(`//
|
|
1115
|
-
lines.push(`// handler result, no generic enum crossing, no dispatch on a dynamic value on the covered plane. Node`);
|
|
1427
|
+
lines.push(`// whose node_* methods take the node's native ports struct and return the node's result WIRE`);
|
|
1428
|
+
lines.push(`// (Self::Wire: WireValue). The runner builds each ports struct by direct native construction (a`);
|
|
1429
|
+
lines.push(`// simple string port lowers to a native Rust expr), dispatches the concrete node_* method, and`);
|
|
1430
|
+
lines.push(`// de-boxes the result wire INLINE (match wire.as_*() { … } fully unrolled — no decode helper) into`);
|
|
1431
|
+
lines.push(`// the node's outType struct cell — no boxed handler result, no generic enum crossing, no dispatch on a dynamic value on the covered plane. Node`);
|
|
1116
1432
|
lines.push(`// results are typed struct cells; a relation child reads the parent's REAL struct result via`);
|
|
1117
1433
|
lines.push(`// direct field access (child-present decision from the real parent value — relationSingle /`);
|
|
1118
1434
|
lines.push(`// connection converge). A real-concurrency stage (bc#87) is static parallel orchestration —`);
|
|
@@ -1216,22 +1532,33 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1216
1532
|
// directly (no `.await`). The runner being `async fn` does not force every call site to await.
|
|
1217
1533
|
const awaitSfx = ap.nodeIsAsync(node.id) ? ".await" : "";
|
|
1218
1534
|
lines.push(`${indent}let ports_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
lines.push(`${indent}};`);
|
|
1535
|
+
// uniform seam: every covered node returns a single result WIRE (Self::Wire); the inline de-box
|
|
1536
|
+
// (renderResultDeBox) classifies it against the STATICALLY declared result type and produces the
|
|
1537
|
+
// structured Error Value on a mismatch (byte-equal codes to run_behavior's outType check, so ≡ holds).
|
|
1223
1538
|
if (meta.policy === "continue") {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1539
|
+
// continue: a failed node (leaf failure OR de-box mismatch) produces no Port — the downstream Skip
|
|
1540
|
+
// propagation is what `continue` means. The inline de-box's mismatch breaks the labeled block (a
|
|
1541
|
+
// local skip, NOT a runner-wide propagate); only a clean decode commits. (Covered componentRef nodes
|
|
1542
|
+
// are always 'fail' policy today — this arm is defensive.)
|
|
1543
|
+
const label = `debox_${s}`;
|
|
1544
|
+
const contFail = (e) => `break '${label} Err(${e})`;
|
|
1545
|
+
lines.push(`${indent}if let Ok(wire_${s}) = handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
|
|
1546
|
+
lines.push(`${indent} let decoded_${s}: Result<${renderTypeRef(ref)}, BehaviorError> = '${label}: {`);
|
|
1547
|
+
lines.push(`${indent} Ok(${renderResultDeBox(ref, `wire_${s}`, node.id, plan, `${indent} `, contFail)})`);
|
|
1548
|
+
lines.push(`${indent} };`);
|
|
1549
|
+
lines.push(`${indent} if let Ok(val_${s}) = decoded_${s} {`);
|
|
1550
|
+
lines.push(`${indent} *${typedCell(node.id)}.borrow_mut() = val_${s};`);
|
|
1551
|
+
lines.push(`${indent} produced_${s}.set(true);`);
|
|
1552
|
+
lines.push(`${indent} }`);
|
|
1227
1553
|
lines.push(`${indent}}`);
|
|
1228
1554
|
}
|
|
1229
1555
|
else {
|
|
1230
|
-
const
|
|
1231
|
-
lines.push(`${indent}
|
|
1232
|
-
lines.push(`${indent}
|
|
1233
|
-
lines.push(`${indent}}
|
|
1234
|
-
lines.push(`${indent}
|
|
1556
|
+
const policyLit = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
|
|
1557
|
+
lines.push(`${indent}let wire_${s} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
|
|
1558
|
+
lines.push(`${indent} Ok(r) => r,`);
|
|
1559
|
+
lines.push(`${indent} Err(e) => return Err(BehaviorError { code: "OP_FAILED".to_string(), message: format!("operation '{}' failed under '${policyLit}: {}", ${rustStrLit(node.id)}, e.message), detail: e.detail }),`);
|
|
1560
|
+
lines.push(`${indent}};`);
|
|
1561
|
+
lines.push(`${indent}*${typedCell(node.id)}.borrow_mut() = ${renderResultDeBox(ref, `wire_${s}`, node.id, plan, indent)};`);
|
|
1235
1562
|
lines.push(`${indent}produced_${s}.set(true);`);
|
|
1236
1563
|
}
|
|
1237
1564
|
for (const c of closers)
|
|
@@ -1356,10 +1683,12 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1356
1683
|
const nMembers = refMembers.length;
|
|
1357
1684
|
const boundN = Math.min(concurrency, nMembers);
|
|
1358
1685
|
const jobsVar = `jobs_${sanitize(comp.name)}`;
|
|
1686
|
+
const hWire = `<H as ${handlerTraitName(comp.name)}>::Wire`;
|
|
1359
1687
|
for (const i of refMembers) {
|
|
1360
1688
|
const s = sanitize(comp.body[i].id);
|
|
1361
|
-
|
|
1362
|
-
|
|
1689
|
+
// every member's thread returns the result WIRE (a single Self::Wire); the ascending COMMIT phase
|
|
1690
|
+
// de-boxes it (so the wire — not the decode — is what crosses the scoped threads).
|
|
1691
|
+
lines.push(` let slot_${s}: std::sync::Mutex<Option<Result<${hWire}, BehaviorError>>> = std::sync::Mutex::new(None);`);
|
|
1363
1692
|
}
|
|
1364
1693
|
lines.push(` let mut ${jobsVar}: Vec<usize> = Vec::new();`);
|
|
1365
1694
|
refMembers.forEach((i, slot) => {
|
|
@@ -1397,22 +1726,29 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1397
1726
|
const s = sanitize(node.id);
|
|
1398
1727
|
const ref = typedNodes.get(node.id);
|
|
1399
1728
|
lines.push(` if ports_${s}.is_some() {`);
|
|
1400
|
-
lines.push(` let
|
|
1401
|
-
lines.push(` Some(Some(r)) => r,`);
|
|
1402
|
-
lines.push(` _ => return Err(unknown_component(${rustStrLit(node.component)})),`);
|
|
1403
|
-
lines.push(` };`);
|
|
1729
|
+
lines.push(` let out_${s} = slot_${s}.lock().unwrap().take().expect("runnable member wrote its slot");`);
|
|
1404
1730
|
if (meta.policy === "continue") {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1731
|
+
// continue: a failed member (leaf failure OR de-box mismatch) commits no Port (defensive — covered
|
|
1732
|
+
// componentRef members are always 'fail' policy today).
|
|
1733
|
+
const label = `debox_${s}`;
|
|
1734
|
+
const contFail = (e) => `break '${label} Err(${e})`;
|
|
1735
|
+
lines.push(` if let Ok(wire_${s}) = out_${s} {`);
|
|
1736
|
+
lines.push(` let decoded_${s}: Result<${renderTypeRef(ref)}, BehaviorError> = '${label}: {`);
|
|
1737
|
+
lines.push(` Ok(${renderResultDeBox(ref, `wire_${s}`, node.id, plan, " ", contFail)})`);
|
|
1738
|
+
lines.push(` };`);
|
|
1739
|
+
lines.push(` if let Ok(val_${s}) = decoded_${s} {`);
|
|
1740
|
+
lines.push(` *${typedCell(node.id)}.borrow_mut() = val_${s};`);
|
|
1741
|
+
lines.push(` produced_${s}.set(true);`);
|
|
1742
|
+
lines.push(` }`);
|
|
1408
1743
|
lines.push(` }`);
|
|
1409
1744
|
}
|
|
1410
1745
|
else {
|
|
1411
|
-
const
|
|
1412
|
-
lines.push(`
|
|
1413
|
-
lines.push(`
|
|
1414
|
-
lines.push(`
|
|
1415
|
-
lines.push(`
|
|
1746
|
+
const policyLit = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
|
|
1747
|
+
lines.push(` let wire_${s} = match out_${s} {`);
|
|
1748
|
+
lines.push(` Ok(r) => r,`);
|
|
1749
|
+
lines.push(` Err(e) => return Err(BehaviorError { code: "OP_FAILED".to_string(), message: format!("operation '{}' failed under '${policyLit}: {}", ${rustStrLit(node.id)}, e.message), detail: e.detail }),`);
|
|
1750
|
+
lines.push(` };`);
|
|
1751
|
+
lines.push(` *${typedCell(node.id)}.borrow_mut() = ${renderResultDeBox(ref, `wire_${s}`, node.id, plan, " ")};`);
|
|
1416
1752
|
lines.push(` produced_${s}.set(true);`);
|
|
1417
1753
|
}
|
|
1418
1754
|
lines.push(` }`);
|
|
@@ -1449,18 +1785,28 @@ isAsync = false) {
|
|
|
1449
1785
|
const portNames = Object.keys(m.ports);
|
|
1450
1786
|
const elemArrRef = typedNodes.get(node.id);
|
|
1451
1787
|
const elemName = renderTypeRef(elemArrRef.elem);
|
|
1452
|
-
|
|
1788
|
+
// each element is de-boxed strictly INLINE: the handler returns the per-element WIRE and the arm decodes
|
|
1789
|
+
// it against the element ref (the `into` field's type for map...into, else the element type). A de-box
|
|
1790
|
+
// mismatch fails closed regardless of elementPolicy (matching the interpreter's whole-node outType throw).
|
|
1791
|
+
const elemDeBoxRef = mapElemRowRef(node, elemArrRef, plan);
|
|
1792
|
+
const renderElemDeBox = (il) => renderResultDeBox(elemDeBoxRef, `er_${s}`, node.id, plan, il);
|
|
1453
1793
|
// build the per-element CONCRETE native ports struct. `il` = indent inside the element loop.
|
|
1454
1794
|
const buildPorts = (il) => {
|
|
1455
|
-
const
|
|
1456
|
-
|
|
1795
|
+
const out = [];
|
|
1796
|
+
const inits = [];
|
|
1797
|
+
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1798
|
+
for (const pn of portNames) {
|
|
1799
|
+
inits.push(emitPortInit(pn, m.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
|
|
1800
|
+
}
|
|
1801
|
+
out.push(`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
1802
|
+
return out;
|
|
1457
1803
|
};
|
|
1458
1804
|
// #108 guard: compile `when` to a native bool over an element-scoped resolveHead ($as element value,
|
|
1459
1805
|
// prior node, input port). On skip: `continue` (element excluded — matches run_behavior's keep-gate).
|
|
1460
1806
|
const emitGuard = (il) => {
|
|
1461
1807
|
if (!hasGuard)
|
|
1462
1808
|
return [];
|
|
1463
|
-
const resolveHead = nativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` });
|
|
1809
|
+
const resolveHead = nativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding: { name: asName, ref: overElemRef, plan }, asBase: `oel_${s}` });
|
|
1464
1810
|
const g = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compileBool(m.when);
|
|
1465
1811
|
const out = [];
|
|
1466
1812
|
for (const st of g.stmts)
|
|
@@ -1500,27 +1846,26 @@ isAsync = false) {
|
|
|
1500
1846
|
lines.push(`${indent} let want_${s} = items_${s}.len();`);
|
|
1501
1847
|
lines.push(`${indent} let bp_${s} = ${batch} { items: items_${s} };`);
|
|
1502
1848
|
lines.push(`${indent} let row_${s} = match handlers.${nodeMethodName(node.id)}(&bp_${s}, None)${awaitSfx} {`);
|
|
1503
|
-
lines.push(`${indent}
|
|
1504
|
-
lines.push(`${indent}
|
|
1849
|
+
lines.push(`${indent} Ok(r) => r,`);
|
|
1850
|
+
lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
|
|
1505
1851
|
lines.push(`${indent} };`);
|
|
1506
|
-
lines.push(`${indent} if row_${s}.
|
|
1507
|
-
lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
|
|
1508
|
-
lines.push(`${indent} }`);
|
|
1509
|
-
lines.push(`${indent} if row_${s}.rows.len() != want_${s} {`);
|
|
1852
|
+
lines.push(`${indent} if row_${s}.len() != want_${s} {`);
|
|
1510
1853
|
lines.push(`${indent} return Err(BehaviorError::new("MAP_BATCH_RESULT_MISMATCH", format!("map '{}': batched handler must return a list aligned to items (want {})", ${rustStrLit(node.id)}, want_${s})));`);
|
|
1511
1854
|
lines.push(`${indent} }`);
|
|
1512
1855
|
if (hasInto) {
|
|
1513
|
-
// zip the aligned
|
|
1514
|
-
|
|
1856
|
+
// zip the aligned per-element WIRES onto each KEPT over element: de-box the element wire strictly
|
|
1857
|
+
// (into the `into` field's type) then materialize the augmented element.
|
|
1858
|
+
lines.push(`${indent} let mut rows_iter_${s} = row_${s}.into_iter();`);
|
|
1515
1859
|
lines.push(`${indent} for over_el_${s} in kept_${s}.iter() {`);
|
|
1516
1860
|
lines.push(`${indent} let er_${s} = rows_iter_${s}.next().expect("aligned batch row");`);
|
|
1517
|
-
lines.push(`${indent}
|
|
1861
|
+
lines.push(`${indent} let dec_${s} = ${renderElemDeBox(`${indent} `)};`);
|
|
1862
|
+
lines.push(`${indent} built_${s}.push(${elemMat}(over_el_${s}, dec_${s}));`);
|
|
1518
1863
|
lines.push(`${indent} }`);
|
|
1519
1864
|
}
|
|
1520
1865
|
else {
|
|
1521
|
-
// no-into:
|
|
1522
|
-
lines.push(`${indent} for er_${s} in row_${s}.
|
|
1523
|
-
lines.push(`${indent}
|
|
1866
|
+
// no-into: de-box each aligned per-element wire DIRECTLY into the element (result = kept elements).
|
|
1867
|
+
lines.push(`${indent} for er_${s} in row_${s}.into_iter() {`);
|
|
1868
|
+
lines.push(`${indent} let el_${s} = ${renderElemDeBox(`${indent} `)};`);
|
|
1524
1869
|
lines.push(`${indent} built_${s}.push(el_${s});`);
|
|
1525
1870
|
lines.push(`${indent} }`);
|
|
1526
1871
|
}
|
|
@@ -1533,20 +1878,26 @@ isAsync = false) {
|
|
|
1533
1878
|
lines.push(l);
|
|
1534
1879
|
for (const l of buildPorts(indent + " "))
|
|
1535
1880
|
lines.push(l);
|
|
1881
|
+
// Element Error Policy (scp-error.md): `skip` drops the failing element and proceeds (order is
|
|
1882
|
+
// preserved; the leaf keeps the Error Value it produced). `error` (default) promotes the element
|
|
1883
|
+
// Failure to the map's Component Failure, carrying the leaf's detail through.
|
|
1536
1884
|
lines.push(`${indent} let er_${s} = match handlers.${nodeMethodName(node.id)}(&ep_${s}, None)${awaitSfx} {`);
|
|
1537
|
-
lines.push(`${indent}
|
|
1538
|
-
|
|
1885
|
+
lines.push(`${indent} Ok(r) => r,`);
|
|
1886
|
+
if (m.elementPolicy === "skip") {
|
|
1887
|
+
lines.push(`${indent} Err(_) => continue,`);
|
|
1888
|
+
}
|
|
1889
|
+
else {
|
|
1890
|
+
lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
|
|
1891
|
+
}
|
|
1539
1892
|
lines.push(`${indent} };`);
|
|
1540
|
-
|
|
1541
|
-
lines.push(`${indent}
|
|
1542
|
-
lines.push(`${indent} }`);
|
|
1893
|
+
// the element's wire is de-boxed strictly INLINE (fail-closed regardless of elementPolicy).
|
|
1894
|
+
lines.push(`${indent} let dec_${s} = ${renderElemDeBox(`${indent} `)};`);
|
|
1543
1895
|
if (hasInto) {
|
|
1544
|
-
lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s},
|
|
1896
|
+
lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s}, dec_${s}));`);
|
|
1545
1897
|
}
|
|
1546
1898
|
else {
|
|
1547
1899
|
lines.push(`${indent} let _ = &oel_${s};`);
|
|
1548
|
-
lines.push(`${indent} ${
|
|
1549
|
-
lines.push(`${indent} built_${s}.push(el_${s});`);
|
|
1900
|
+
lines.push(`${indent} built_${s}.push(dec_${s});`);
|
|
1550
1901
|
}
|
|
1551
1902
|
lines.push(`${indent}}`);
|
|
1552
1903
|
}
|
|
@@ -1583,9 +1934,12 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
|
|
|
1583
1934
|
if (!dkField || dkField.type.kind !== "scalar" || dkField.type.scalar !== "string")
|
|
1584
1935
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust fanout (v3): node '${node.id}' dedupeKey '${f.dedupeKey}' must be a string field of the connection items element type '${elemRef.name}'.`);
|
|
1585
1936
|
const dkRust = rustFieldName(f.dedupeKey);
|
|
1586
|
-
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1587
1937
|
const buildPorts = (il) => {
|
|
1588
|
-
const inits =
|
|
1938
|
+
const inits = [];
|
|
1939
|
+
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1940
|
+
for (const pn of portNames) {
|
|
1941
|
+
inits.push(emitPortInit(pn, f.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
|
|
1942
|
+
}
|
|
1589
1943
|
return [`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`];
|
|
1590
1944
|
};
|
|
1591
1945
|
const lines = [];
|
|
@@ -1609,21 +1963,24 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
|
|
|
1609
1963
|
lines.push(`${indent} let want_${s} = pi_${s}.len();`);
|
|
1610
1964
|
lines.push(`${indent} let bp_${s} = ${batch} { items: pi_${s} };`);
|
|
1611
1965
|
lines.push(`${indent} let row_${s} = match handlers.${nodeMethodName(node.id)}(&bp_${s}, None)${awaitSfx} {`);
|
|
1612
|
-
lines.push(`${indent}
|
|
1613
|
-
lines.push(`${indent}
|
|
1966
|
+
lines.push(`${indent} Ok(r) => r,`);
|
|
1967
|
+
lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
|
|
1614
1968
|
lines.push(`${indent} };`);
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
lines.push(`${indent} if row_${s}.
|
|
1969
|
+
// the handler returns the aligned per-id element WIRES (Vec<Option<Self::Wire>>); each present body is
|
|
1970
|
+
// de-boxed strictly INLINE (a de-box mismatch fails closed). A DANGLING id is None → the zero element
|
|
1971
|
+
// (empty dedupeKey), which the dangling drop excludes.
|
|
1972
|
+
lines.push(`${indent} if row_${s}.len() != want_${s} {`);
|
|
1619
1973
|
lines.push(`${indent} return Err(BehaviorError::new("FANOUT_BATCH_RESULT_MISMATCH", format!("fanout '{}': batched handler must return a list aligned to the deduped id list (want {})", ${rustStrLit(node.id)}, want_${s})));`);
|
|
1620
1974
|
lines.push(`${indent} }`);
|
|
1621
1975
|
// ── THE ONE dedupe/drop (mirrors fanoutDedupDrop verbatim): first-seen dedupe by dedupeKey field;
|
|
1622
1976
|
// drop dangling (empty dedupeKey ≡ interpreter's null/absent-key body); implicitSource strip is
|
|
1623
1977
|
// structural (the elem type already omits it). cursor is always None (connection wrap).
|
|
1624
1978
|
lines.push(`${indent} let mut seen_${s}: std::collections::HashSet<String> = std::collections::HashSet::new();`);
|
|
1625
|
-
lines.push(`${indent} for
|
|
1626
|
-
lines.push(`${indent}
|
|
1979
|
+
lines.push(`${indent} for opt_er_${s} in row_${s}.into_iter() {`);
|
|
1980
|
+
lines.push(`${indent} let el_${s}: ${elemName} = match opt_er_${s} {`);
|
|
1981
|
+
lines.push(`${indent} Some(wire_${s}) => ${renderResultDeBox(elemRef, `wire_${s}`, node.id, plan, `${indent} `)},`);
|
|
1982
|
+
lines.push(`${indent} None => Default::default(),`);
|
|
1983
|
+
lines.push(`${indent} };`);
|
|
1627
1984
|
lines.push(`${indent} let key_${s} = el_${s}.${dkRust}.clone();`);
|
|
1628
1985
|
if (f.drop === "dangling") {
|
|
1629
1986
|
lines.push(`${indent} if key_${s}.is_empty() { continue; }`);
|
|
@@ -1639,73 +1996,6 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
|
|
|
1639
1996
|
lines.push(c);
|
|
1640
1997
|
return lines.join("\n");
|
|
1641
1998
|
}
|
|
1642
|
-
/**
|
|
1643
|
-
* rustOwn — render the OWNED form of a resolved ref value at the LEAF (native-expr calls this after the
|
|
1644
|
-
* field path is walked). `guard` marks a value read through a `RefCell`/`&` borrow (a prior node's
|
|
1645
|
-
* `cell.borrow()`, a `$as` element `&Elem`): a WHOLE-node Copy scalar dereferences (`*expr`), while a
|
|
1646
|
-
* Copy FIELD is already a copy-out of the auto-deref'd place. A String/struct/map clones; an array clones
|
|
1647
|
-
* only when the position OWNS it (a port field) and stays borrowed for a `len`. Cloning a Copy value
|
|
1648
|
-
* would trip clippy's `clone_on_copy`, so Copy never clones.
|
|
1649
|
-
*/
|
|
1650
|
-
function rustOwn(guard, ownArrays) {
|
|
1651
|
-
return (expr, ref, fieldAccessed) => {
|
|
1652
|
-
if (ref.kind === "arr")
|
|
1653
|
-
return ownArrays ? `${expr}.clone()` : expr;
|
|
1654
|
-
if (typeRefIsCopy(ref))
|
|
1655
|
-
return guard && !fieldAccessed ? `*${expr}` : expr;
|
|
1656
|
-
return `${expr}.clone()`;
|
|
1657
|
-
};
|
|
1658
|
-
}
|
|
1659
|
-
/**
|
|
1660
|
-
* nativeResolveHead — the ONE ref-head resolver the runtime-free native-expr compiler reads, for cond
|
|
1661
|
-
* `if`/branches, a map/fanout `when` guard, AND a port. A head is a `$as` element binding, a prior body
|
|
1662
|
-
* node's typed cell, or an input port — each a native base expr of the head's TypeRef PLUS an `own`
|
|
1663
|
-
* renderer (rustOwn) that native-expr applies at the resolved LEAF. Ownership lives at the leaf so a
|
|
1664
|
-
* prior-node field ref clones only the field (`cell.borrow().items.clone()`), never the whole struct.
|
|
1665
|
-
* `isPrior` tests whether a head names a settled prior node (position-aware at a runner site; structural
|
|
1666
|
-
* at coverage/struct-def). `asBase` is the element base for a `$as`-headed ref; `ownArrays` owns an array
|
|
1667
|
-
* (a port field) vs borrows it (a cond `len`).
|
|
1668
|
-
*/
|
|
1669
|
-
function nativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
|
|
1670
|
-
const ownArrays = opts?.ownArrays === true;
|
|
1671
|
-
return (head) => {
|
|
1672
|
-
// a `$as` element (`for oel in vec.iter()`) is a `&Elem` — a borrow guard.
|
|
1673
|
-
if (opts?.asBinding !== undefined && opts.asBase !== undefined && head === opts.asBinding.name) {
|
|
1674
|
-
return { expr: opts.asBase, ref: opts.asBinding.ref, own: rustOwn(true, ownArrays) };
|
|
1675
|
-
}
|
|
1676
|
-
// a prior node's value lives behind a `RefCell` borrow guard.
|
|
1677
|
-
if (isPrior(head)) {
|
|
1678
|
-
const ref = typedNodes.get(head);
|
|
1679
|
-
return { expr: `${typedCell(head)}.borrow()`, ref, own: rustOwn(true, ownArrays) };
|
|
1680
|
-
}
|
|
1681
|
-
// an input port is a by-value struct field (no guard).
|
|
1682
|
-
const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
|
|
1683
|
-
if (ref === undefined)
|
|
1684
|
-
return null;
|
|
1685
|
-
return { expr: `in_.${rustFieldName(head)}`, ref, own: rustOwn(false, ownArrays) };
|
|
1686
|
-
};
|
|
1687
|
-
}
|
|
1688
|
-
/**
|
|
1689
|
-
* compilePortNode — lower a port expression to its native ports-struct field via the runtime-free
|
|
1690
|
-
* native-expr compiler (native-expr.ts): the ONE port lowering. The port's field TYPE is the compiled
|
|
1691
|
-
* `ref`, its initializer the compiled `expr` — so the two are derived from a SINGLE lowering and can
|
|
1692
|
-
* never disagree. Any closed operator, a literal, a ref (input scalar/array/map, prior-node scalar/array,
|
|
1693
|
-
* `$as` element field), a concat, a static array, an optional-input read — all resolve here. A port that
|
|
1694
|
-
* cannot lower to a native value FAILS CLOSED loudly. A lowering that is FALLIBLE (hoists statements) is
|
|
1695
|
-
* a covered SHAPE returned here as-is; only the value site (emitPortInit) rejects it, since a ports-struct
|
|
1696
|
-
* literal cannot host statements — the coverage gate/type site must still see it as covered.
|
|
1697
|
-
*/
|
|
1698
|
-
function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
|
|
1699
|
-
const resolveHead = nativeResolveHead(comp, plan, typedNodes, isPrior, { ...opts, ownArrays: true });
|
|
1700
|
-
try {
|
|
1701
|
-
return new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compilePort(node);
|
|
1702
|
-
}
|
|
1703
|
-
catch (e) {
|
|
1704
|
-
if (!(e instanceof GeneratorFailure))
|
|
1705
|
-
throw e;
|
|
1706
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} ${JSON.stringify(node)} does not lower to a native Rust value: ${e.message}`);
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
1999
|
/**
|
|
1710
2000
|
* emitCondArm — the struct-native exec of a covered cond node (#108, rust). Mirrors run_behavior's cond
|
|
1711
2001
|
* lower: compile `if` to a native bool, materialize the TAKEN branch to the node's outType (only the
|
|
@@ -1768,16 +2058,6 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
|
|
|
1768
2058
|
lines.push(cl);
|
|
1769
2059
|
return lines.join("\n");
|
|
1770
2060
|
}
|
|
1771
|
-
/** emitElemMove — move a concrete element row (RawElemNR, error already handled) into a fresh `let` local
|
|
1772
|
-
* of the element outType. Named → struct literal from moved fields; scalar/arr/opt → .val. */
|
|
1773
|
-
function emitElemMove(letBind, rowExpr, ref, plan) {
|
|
1774
|
-
if (ref.kind === "named") {
|
|
1775
|
-
const decl = findDecl(plan, ref.name);
|
|
1776
|
-
const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: ${rowExpr}.${rustFieldName(f.name)}`).join(", ");
|
|
1777
|
-
return `${letBind} = ${ref.name} { ${inits} };`;
|
|
1778
|
-
}
|
|
1779
|
-
return `${letBind} = ${rowExpr}.val;`;
|
|
1780
|
-
}
|
|
1781
2061
|
/**
|
|
1782
2062
|
* emitProducedAwareValue — the #86 part 1 produced-aware output lowering (rust twin of the go one).
|
|
1783
2063
|
*/
|
|
@@ -1847,6 +2127,13 @@ pub const COMPONENT_NAMES_NATIVE_RAW: [&str; ${names.length}] = [${names.join(",
|
|
|
1847
2127
|
const UNKNOWN_COMPONENT_HELPER = `#[allow(dead_code)]
|
|
1848
2128
|
fn unknown_component(component: &str) -> BehaviorError {
|
|
1849
2129
|
BehaviorError::new("UNKNOWN_COMPONENT", format!("component '{component}' has no handler (fail-closed)"))
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// leaf_failure — a failure the LEAF reports (test glue): the runner assigns the node's failure code
|
|
2133
|
+
// from its Error Policy Kind, so what a leaf carries is its message and its structured Error Value.
|
|
2134
|
+
#[allow(dead_code)]
|
|
2135
|
+
fn leaf_failure(message: impl Into<String>) -> BehaviorError {
|
|
2136
|
+
BehaviorError::new("LEAF_FAILURE", message)
|
|
1850
2137
|
}`;
|
|
1851
2138
|
// ── module assembly ──────────────────────────────────────────────────────────────────
|
|
1852
2139
|
function emit(ctx) {
|
|
@@ -1855,20 +2142,17 @@ function emit(ctx) {
|
|
|
1855
2142
|
// rust CONSUMES this annotation (async fn + .await where derived); go ignores it. per-node granularity
|
|
1856
2143
|
// is a strict generalization of increment-1's uniform ioModel:'async' (= every terminal async).
|
|
1857
2144
|
const asyncPlans = buildAsyncPlans(ctx.ir, ctx.ioModel);
|
|
1858
|
-
// The type plan is the SSoT the coverage gate + the port lowering both read (the gate ASKS the port
|
|
1859
|
-
// lowering whether a port lowers), so it is built up-front — before the coverage checks.
|
|
1860
|
-
const plan = buildTypePlan(ctx.ir);
|
|
1861
2145
|
for (const c of ctx.ir.components)
|
|
1862
|
-
assertTypedCoverage(c
|
|
1863
|
-
const native = ctx.ir.components.filter(
|
|
1864
|
-
const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c
|
|
2146
|
+
assertTypedCoverage(c);
|
|
2147
|
+
const native = ctx.ir.components.filter(isNativeRaw);
|
|
2148
|
+
const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
|
|
1865
2149
|
// FAIL CLOSED (bc#86/#89): the --typed-native (native codegen) surface is native-only. Previously a
|
|
1866
2150
|
// non-covered component was SILENTLY DROPPED (emitted only as a comment note), concealing that it
|
|
1867
2151
|
// never got native codegen. We now throw a LOUD GeneratorFailure naming every uncovered component +
|
|
1868
2152
|
// its specific reject reason. A fully-covered module is unchanged.
|
|
1869
2153
|
if (nonNative.length > 0) {
|
|
1870
2154
|
const rejects = nonNative
|
|
1871
|
-
.map((c) => ` - component '${c.name}': ${coverageRejectReason(c
|
|
2155
|
+
.map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
|
|
1872
2156
|
.join("\n");
|
|
1873
2157
|
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (native codegen) surface: ${nonNative.length} component(s) are NOT a covered native read:\n${rejects}\n\n` +
|
|
1874
2158
|
`The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
|
|
@@ -1882,6 +2166,7 @@ function emit(ctx) {
|
|
|
1882
2166
|
${emitStructSurface([])}
|
|
1883
2167
|
`;
|
|
1884
2168
|
}
|
|
2169
|
+
const plan = buildTypePlan(ctx.ir);
|
|
1885
2170
|
// The typed-native module is a de-boxed CONSUMER artifact: the covered runner RETURNS these outType
|
|
1886
2171
|
// structs and the consumer reads their fields natively. So the decls (and their FIELDS) are pub — the
|
|
1887
2172
|
// consumer keeps the model native (mirrors Go's exported outType struct fields on the covered path).
|
|
@@ -1894,12 +2179,18 @@ ${emitStructSurface([])}
|
|
|
1894
2179
|
// #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler).
|
|
1895
2180
|
rustExprUsed.helpers = false;
|
|
1896
2181
|
const structs = [];
|
|
1897
|
-
const rowStructs = [];
|
|
1898
2182
|
const handlerTraits = [];
|
|
1899
2183
|
const inStructs = [];
|
|
1900
2184
|
const elemMaterializers = [];
|
|
2185
|
+
// every covered node returns a WireValue the inline de-box classifies — the wire seam (probe enums +
|
|
2186
|
+
// WireValue/WireRow/WireList traits) is emitted once whenever any handler node exists.
|
|
2187
|
+
const anyWire = native.some((c) => c.body.some((n) => !("cond" in n)));
|
|
1901
2188
|
for (const c of native) {
|
|
1902
|
-
const typedNodes =
|
|
2189
|
+
const typedNodes = new Map();
|
|
2190
|
+
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
2191
|
+
for (const n of c.body)
|
|
2192
|
+
if (!isControlGate(n))
|
|
2193
|
+
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan)); // #125: control-gate typeless; map は produced-array ref に正規化
|
|
1903
2194
|
for (const n of c.body) {
|
|
1904
2195
|
if ("fanout" in n)
|
|
1905
2196
|
structs.push(emitFanoutPortsStructs(c, n, typedNodes, plan));
|
|
@@ -1909,9 +2200,8 @@ ${emitStructSurface([])}
|
|
|
1909
2200
|
// #108: a cond node has NO ports struct (pure Expression — no handler dispatch).
|
|
1910
2201
|
}
|
|
1911
2202
|
else
|
|
1912
|
-
structs.push(emitPortsStruct(c, n, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
|
|
2203
|
+
structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
|
|
1913
2204
|
}
|
|
1914
|
-
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
1915
2205
|
handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
|
|
1916
2206
|
inStructs.push(emitInStruct(c, plan));
|
|
1917
2207
|
const mm = emitMapElemMaterializers(c, typedNodes, plan);
|
|
@@ -1927,9 +2217,9 @@ ${emitStructSurface([])}
|
|
|
1927
2217
|
// relationKind:single children), emitted ONLY as a STRUCT-returning runner GENERIC over a per-component
|
|
1928
2218
|
// CONCRETE handler trait (HandlerNR<comp>): native ports struct IN (direct construction — every port is a
|
|
1929
2219
|
// CONCRETE Rust value: String / bool / Vec<&str> projection / i64 limit — NO boxed Value, NO by-name
|
|
1930
|
-
// accessor); the handler takes a typed produced-aware 'bound' Option and returns the node's
|
|
1931
|
-
//
|
|
1932
|
-
// outType struct. The covered plane carries NO bc-runtime reference at all — failures use the LOCAL
|
|
2220
|
+
// accessor); the handler takes a typed produced-aware 'bound' Option and returns the node's result WIRE
|
|
2221
|
+
// (Self::Wire: WireValue), and the runner de-boxes it INLINE (fully-unrolled probe/match — no decode helper,
|
|
2222
|
+
// no output recursion) into the node's outType struct. The covered plane carries NO bc-runtime reference at all — failures use the LOCAL
|
|
1933
2223
|
// BehaviorError (same codes, byte-equal to run_behavior). INLINE sequential (no plan driver) so a relation
|
|
1934
2224
|
// child reads the parent's REAL struct result via direct field access and the child-present decision is
|
|
1935
2225
|
// made from the real parent value — relationSingle / connection converge with run_behavior (fixes
|
|
@@ -1950,11 +2240,11 @@ use std::cell::RefCell;`;
|
|
|
1950
2240
|
structs.some((r) => r.trim().length > 0)
|
|
1951
2241
|
? `// Native ports structs (one per componentRef node; typed per the static port type — CONCRETE).\n${structs.filter((r) => r.trim().length > 0).join("\n\n")}`
|
|
1952
2242
|
: undefined,
|
|
1953
|
-
rowStructs.some((r) => r.trim().length > 0)
|
|
1954
|
-
? `// CONCRETE per-node row structs (typed fields = the node's outType; + error signal).\n${rowStructs.filter((r) => r.trim().length > 0).join("\n\n")}`
|
|
1955
|
-
: undefined,
|
|
1956
2243
|
`// CONCRETE per-component input structs (fields = inputPorts).\n${inStructs.join("\n\n")}`,
|
|
1957
|
-
`// CONCRETE per-component handler traits (one node_* method per node — native ports IN,
|
|
2244
|
+
`// CONCRETE per-component handler traits (one node_* method per node — native ports IN, WIRE value OUT).\n${handlerTraits.join("\n\n")}`,
|
|
2245
|
+
anyWire
|
|
2246
|
+
? `// strict de-box wire seam — the consumer implements WireValue (+ WireRow/WireList reached via\n// as_row/as_list) over its wire payload (variant classification); the generated INLINE de-box owns\n// strictness (required/optional, present/absent, error assembly, fail-closed). Concrete enums +\n// monomorphized traits — no boxed value, no trait object, no heap allocation crosses the seam.\n${emitRustProbeWireTypes()}`
|
|
2247
|
+
: undefined,
|
|
1958
2248
|
elemMaterializers.length
|
|
1959
2249
|
? `// map augmented-element copiers (pure struct field assignment — no dynamic value read).\n${elemMaterializers.join("\n\n")}`
|
|
1960
2250
|
: undefined,
|
|
@@ -1974,8 +2264,8 @@ use std::cell::RefCell;`;
|
|
|
1974
2264
|
*/
|
|
1975
2265
|
export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
1976
2266
|
const components = ir.components;
|
|
2267
|
+
const native = components.filter(isNativeRaw);
|
|
1977
2268
|
const plan = buildTypePlan(ir);
|
|
1978
|
-
const native = components.filter((c) => isNativeRaw(c, plan));
|
|
1979
2269
|
const needSync = native.some((c) => concurrencyPlan(c) !== null);
|
|
1980
2270
|
const serializers = emitSerializers(plan);
|
|
1981
2271
|
const marshallers = emitMarshallers(plan);
|
|
@@ -1986,10 +2276,182 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
1986
2276
|
const anyAsyncRunner = native.some((c) => asyncPlans.get(c.name).runnerIsAsync);
|
|
1987
2277
|
// ── per-component input decoders: generic &[(String,Value)] -> concrete InNR_<comp> (test glue). ──
|
|
1988
2278
|
const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
|
|
1989
|
-
//
|
|
1990
|
-
//
|
|
1991
|
-
const
|
|
1992
|
-
|
|
2279
|
+
// every covered node returns a WireValue the inline de-box classifies — so the scripted wire adapter is
|
|
2280
|
+
// needed whenever any component has a handler node.
|
|
2281
|
+
const anyDeBox = native.some((c) => c.body.some((n) => !("cond" in n)));
|
|
2282
|
+
// scripted wire adapter (TEST glue): wraps a scripted Value as a WireValue (+ WireRow/WireList reached
|
|
2283
|
+
// via as_row/as_list) and classifies each datum's native type into a DynamoDB-flavored wire tag
|
|
2284
|
+
// (S/N/BOOL/L/M/NULL). A real consumer implements WireValue over its own AttributeValue payload; this
|
|
2285
|
+
// drives the generated inline de-box from the same scripted vectors run_behavior uses — so the
|
|
2286
|
+
// actualWireType observed in a mismatch is the PRODUCER tag.
|
|
2287
|
+
const wireAdapter = anyDeBox
|
|
2288
|
+
? `// ── scripted wire adapter (TEST glue) ──
|
|
2289
|
+
fn scripted_wire_tag(v: &Value) -> String {
|
|
2290
|
+
match v {
|
|
2291
|
+
Value::Null => "NULL",
|
|
2292
|
+
Value::Bool(_) => "BOOL",
|
|
2293
|
+
Value::Int(_) | Value::Float(_) => "N",
|
|
2294
|
+
Value::Str(_) => "S",
|
|
2295
|
+
Value::Arr(_) => "L",
|
|
2296
|
+
Value::Obj(_) => "M",
|
|
2297
|
+
}
|
|
2298
|
+
.to_string()
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
fn scripted_wire_raw(v: &Value) -> String {
|
|
2302
|
+
match v {
|
|
2303
|
+
Value::Null => "null".to_string(),
|
|
2304
|
+
Value::Bool(b) => b.to_string(),
|
|
2305
|
+
Value::Int(n) => n.to_string(),
|
|
2306
|
+
Value::Float(f) => f.to_string(),
|
|
2307
|
+
Value::Str(s) => s.clone(),
|
|
2308
|
+
Value::Arr(_) | Value::Obj(_) => "[composite]".to_string(),
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
fn probe_string_val(v: Option<&Value>) -> Probe<String> {
|
|
2313
|
+
match v {
|
|
2314
|
+
None => Probe::Absent,
|
|
2315
|
+
Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
|
|
2316
|
+
Some(Value::Str(s)) => Probe::Got(s.clone()),
|
|
2317
|
+
Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
fn probe_number_val(v: Option<&Value>) -> NumProbe {
|
|
2322
|
+
match v {
|
|
2323
|
+
None => NumProbe::Absent,
|
|
2324
|
+
Some(Value::Null) => NumProbe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
|
|
2325
|
+
Some(Value::Int(n)) => NumProbe::Got { raw: n.to_string(), actual_wire_type: "N".to_string() },
|
|
2326
|
+
Some(Value::Float(f)) => NumProbe::Got { raw: f.to_string(), actual_wire_type: "N".to_string() },
|
|
2327
|
+
Some(other) => NumProbe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
fn probe_bool_val(v: Option<&Value>) -> Probe<bool> {
|
|
2332
|
+
match v {
|
|
2333
|
+
None => Probe::Absent,
|
|
2334
|
+
Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
|
|
2335
|
+
Some(Value::Bool(b)) => Probe::Got(*b),
|
|
2336
|
+
Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
fn probe_row_val(v: Option<&Value>) -> Probe<ScriptedWireRow> {
|
|
2341
|
+
match v {
|
|
2342
|
+
None => Probe::Absent,
|
|
2343
|
+
Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
|
|
2344
|
+
Some(Value::Obj(pairs)) => Probe::Got(ScriptedWireRow { obj: pairs.clone() }),
|
|
2345
|
+
Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
fn probe_list_val(v: Option<&Value>) -> Probe<ScriptedWireList> {
|
|
2350
|
+
match v {
|
|
2351
|
+
None => Probe::Absent,
|
|
2352
|
+
Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
|
|
2353
|
+
Some(Value::Arr(items)) => Probe::Got(ScriptedWireList { items: items.clone() }),
|
|
2354
|
+
Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
pub struct ScriptedWireRow {
|
|
2359
|
+
obj: Vec<(String, Value)>,
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
impl ScriptedWireRow {
|
|
2363
|
+
pub fn new(v: Value) -> Self {
|
|
2364
|
+
match v {
|
|
2365
|
+
Value::Obj(pairs) => ScriptedWireRow { obj: pairs },
|
|
2366
|
+
_ => ScriptedWireRow { obj: Vec::new() },
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
fn get(&self, field: &str) -> Option<&Value> {
|
|
2370
|
+
self.obj.iter().find(|(k, _)| k.as_str() == field).map(|(_, v)| v)
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
pub struct ScriptedWireList {
|
|
2375
|
+
items: Vec<Value>,
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
// ScriptedWireValue — a scripted node result wrapped as the uniform WireValue: a top value is always
|
|
2379
|
+
// present (Some), so as_* never observes Absent; a scripted null is Value::Null → Probe::Null.
|
|
2380
|
+
pub struct ScriptedWireValue {
|
|
2381
|
+
v: Value,
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
impl ScriptedWireValue {
|
|
2385
|
+
pub fn new(v: Value) -> Self {
|
|
2386
|
+
ScriptedWireValue { v }
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
impl WireValue for ScriptedWireValue {
|
|
2391
|
+
type Row = ScriptedWireRow;
|
|
2392
|
+
fn as_string(&self) -> Probe<String> {
|
|
2393
|
+
probe_string_val(Some(&self.v))
|
|
2394
|
+
}
|
|
2395
|
+
fn as_number(&self) -> NumProbe {
|
|
2396
|
+
probe_number_val(Some(&self.v))
|
|
2397
|
+
}
|
|
2398
|
+
fn as_bool(&self) -> Probe<bool> {
|
|
2399
|
+
probe_bool_val(Some(&self.v))
|
|
2400
|
+
}
|
|
2401
|
+
fn as_row(&self) -> Probe<ScriptedWireRow> {
|
|
2402
|
+
probe_row_val(Some(&self.v))
|
|
2403
|
+
}
|
|
2404
|
+
fn as_list(&self) -> Probe<ScriptedWireList> {
|
|
2405
|
+
probe_list_val(Some(&self.v))
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
impl WireRow for ScriptedWireRow {
|
|
2410
|
+
type List = ScriptedWireList;
|
|
2411
|
+
fn keys(&self) -> Vec<String> {
|
|
2412
|
+
self.obj.iter().map(|(k, _)| k.clone()).collect()
|
|
2413
|
+
}
|
|
2414
|
+
fn probe_string(&self, field: &str) -> Probe<String> {
|
|
2415
|
+
probe_string_val(self.get(field))
|
|
2416
|
+
}
|
|
2417
|
+
fn probe_number(&self, field: &str) -> NumProbe {
|
|
2418
|
+
probe_number_val(self.get(field))
|
|
2419
|
+
}
|
|
2420
|
+
fn probe_bool(&self, field: &str) -> Probe<bool> {
|
|
2421
|
+
probe_bool_val(self.get(field))
|
|
2422
|
+
}
|
|
2423
|
+
fn probe_row(&self, field: &str) -> Probe<Self> {
|
|
2424
|
+
probe_row_val(self.get(field))
|
|
2425
|
+
}
|
|
2426
|
+
fn probe_list(&self, field: &str) -> Probe<Self::List> {
|
|
2427
|
+
probe_list_val(self.get(field))
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
impl WireList for ScriptedWireList {
|
|
2432
|
+
type Row = ScriptedWireRow;
|
|
2433
|
+
fn len(&self) -> usize {
|
|
2434
|
+
self.items.len()
|
|
2435
|
+
}
|
|
2436
|
+
fn elem_string(&self, i: usize) -> Probe<String> {
|
|
2437
|
+
probe_string_val(self.items.get(i))
|
|
2438
|
+
}
|
|
2439
|
+
fn elem_number(&self, i: usize) -> NumProbe {
|
|
2440
|
+
probe_number_val(self.items.get(i))
|
|
2441
|
+
}
|
|
2442
|
+
fn elem_bool(&self, i: usize) -> Probe<bool> {
|
|
2443
|
+
probe_bool_val(self.items.get(i))
|
|
2444
|
+
}
|
|
2445
|
+
fn elem_row(&self, i: usize) -> Probe<Self::Row> {
|
|
2446
|
+
probe_row_val(self.items.get(i))
|
|
2447
|
+
}
|
|
2448
|
+
fn elem_list(&self, i: usize) -> Probe<Self> {
|
|
2449
|
+
probe_list_val(self.items.get(i))
|
|
2450
|
+
}
|
|
2451
|
+
}`
|
|
2452
|
+
: "";
|
|
2453
|
+
// ── concrete scripted handler: implements every HandlerNR<comp> trait by returning the scripted ok
|
|
2454
|
+
// Value as a WireValue (the generic scripted-source queue meets the uniform wire seam). ──
|
|
1993
2455
|
const traitImpls = [];
|
|
1994
2456
|
for (const c of native) {
|
|
1995
2457
|
const ap = asyncPlans.get(c.name);
|
|
@@ -2009,101 +2471,106 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2009
2471
|
// Option<K> (NOT a boxed Value) — computed per node so the impl signature matches the trait.
|
|
2010
2472
|
const bt = boundRustType(n, typedNodes, plan);
|
|
2011
2473
|
if ("fanout" in n) {
|
|
2012
|
-
//
|
|
2013
|
-
//
|
|
2474
|
+
// fanout: return the aligned per-id element WIRES (Vec<Option<ScriptedWireValue>>); a null aligned
|
|
2475
|
+
// body is a DANGLING id → None (the covered runner maps it to the zero element).
|
|
2014
2476
|
const f = n.fanout;
|
|
2015
|
-
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
2016
|
-
const elemT = rawElemStructName(c.name, n.id);
|
|
2017
|
-
const batchT = rawRowStructName(c.name, n.id);
|
|
2018
|
-
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2019
2477
|
const portsT = `${portsStructName(c.name, n.id)}Batch`;
|
|
2020
|
-
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option
|
|
2021
|
-
let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)})?;
|
|
2478
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<Option<ScriptedWireValue>>, BehaviorError> {
|
|
2479
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)}).ok_or_else(|| unknown_component(${rustStrLit(f.component)}))?;
|
|
2022
2480
|
if is_err {
|
|
2023
|
-
return
|
|
2481
|
+
return Err(leaf_failure(err));
|
|
2024
2482
|
}
|
|
2025
2483
|
let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
let rows = arr
|
|
2029
|
-
.iter()
|
|
2484
|
+
let wires = arr
|
|
2485
|
+
.into_iter()
|
|
2030
2486
|
.map(|ev| match ev {
|
|
2031
|
-
Value::Null =>
|
|
2032
|
-
_ =>
|
|
2487
|
+
Value::Null => None,
|
|
2488
|
+
_ => Some(ScriptedWireValue::new(ev)),
|
|
2033
2489
|
})
|
|
2034
2490
|
.collect();
|
|
2035
|
-
|
|
2491
|
+
Ok(wires)
|
|
2036
2492
|
}`);
|
|
2037
2493
|
continue;
|
|
2038
2494
|
}
|
|
2039
2495
|
if ("map" in n) {
|
|
2496
|
+
// map: batched → one WIRE per element (Vec<ScriptedWireValue>); non-batched → one element WIRE.
|
|
2040
2497
|
const m = n.map;
|
|
2041
2498
|
const batched = m.batched === true;
|
|
2042
|
-
const arrRef = ref;
|
|
2043
|
-
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
2044
|
-
const elemT = rawElemStructName(c.name, n.id);
|
|
2045
|
-
const batchT = rawRowStructName(c.name, n.id);
|
|
2046
|
-
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2047
2499
|
const portsT = batched ? `${portsStructName(c.name, n.id)}Batch` : portsStructName(c.name, n.id);
|
|
2048
|
-
const retT = batched ? batchT : elemT;
|
|
2049
2500
|
if (batched) {
|
|
2050
|
-
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) ->
|
|
2051
|
-
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)})?;
|
|
2501
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<ScriptedWireValue>, BehaviorError> {
|
|
2502
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
|
|
2052
2503
|
if is_err {
|
|
2053
|
-
return
|
|
2504
|
+
return Err(leaf_failure(err));
|
|
2054
2505
|
}
|
|
2055
2506
|
let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
|
|
2056
|
-
let
|
|
2057
|
-
|
|
2507
|
+
let wires = arr.into_iter().map(ScriptedWireValue::new).collect();
|
|
2508
|
+
Ok(wires)
|
|
2058
2509
|
}`);
|
|
2059
2510
|
}
|
|
2060
2511
|
else {
|
|
2061
|
-
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) ->
|
|
2062
|
-
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)})?;
|
|
2512
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<ScriptedWireValue, BehaviorError> {
|
|
2513
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
|
|
2063
2514
|
if is_err {
|
|
2064
|
-
return
|
|
2515
|
+
return Err(leaf_failure(err));
|
|
2065
2516
|
}
|
|
2066
|
-
|
|
2067
|
-
Some(${elemDecode}(&v))
|
|
2517
|
+
Ok(ScriptedWireValue::new(ok.unwrap_or(Value::Null)))
|
|
2068
2518
|
}`);
|
|
2069
2519
|
}
|
|
2070
|
-
void retT;
|
|
2071
2520
|
continue;
|
|
2072
2521
|
}
|
|
2522
|
+
// componentRef: return the whole result WIRE (one ScriptedWireValue wrapping the scripted ok Value);
|
|
2523
|
+
// the covered runner's INLINE de-box classifies it. A real consumer returns its own wire payload.
|
|
2073
2524
|
const node = n;
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2525
|
+
// echo (#129/#141 fixtures): the scripted leaf can ECHO an input PORT back as its result wire, so the
|
|
2526
|
+
// input-port value flows into the de-boxed result and the runtime non-vacuity asserts survive.
|
|
2527
|
+
const echoBranches = [];
|
|
2528
|
+
if (ref.kind === "named") {
|
|
2529
|
+
const decl = findDecl(plan, ref.name);
|
|
2530
|
+
const field = decl !== undefined && decl.fields.length === 1 ? decl.fields[0] : undefined;
|
|
2531
|
+
const fieldRust = field !== undefined ? renderTypeRef(field.type) : null;
|
|
2532
|
+
const recordRust = renderTypeRef(ref);
|
|
2533
|
+
for (const pn of Object.keys(node.ports)) {
|
|
2534
|
+
let portRust = null;
|
|
2535
|
+
try {
|
|
2536
|
+
portRust = portFieldRustType(node.ports[pn], c, plan, typedNodes, `echo port '${pn}'`);
|
|
2537
|
+
}
|
|
2538
|
+
catch {
|
|
2539
|
+
portRust = null;
|
|
2540
|
+
}
|
|
2541
|
+
if (portRust === recordRust) {
|
|
2542
|
+
// the port IS the whole record → echo it DIRECTLY (matching run_behavior's plain echo:port).
|
|
2543
|
+
const own = typeRefIsCopy(ref) ? `ports.${portFieldName(pn)}` : `ports.${portFieldName(pn)}.clone()`;
|
|
2544
|
+
echoBranches.push(` if echo == ${rustStrLit(pn)} {\n let tv = ${own};\n return Ok(ScriptedWireValue::new(${serializeTyped("tv", ref, plan)}));\n }`);
|
|
2545
|
+
}
|
|
2546
|
+
else if (fieldRust !== null && field !== undefined && portRust === fieldRust) {
|
|
2547
|
+
// a scalar port wraps into the record's single field ({v: ports.X}).
|
|
2548
|
+
const own = typeRefIsCopy(field.type) ? `ports.${portFieldName(pn)}` : `ports.${portFieldName(pn)}.clone()`;
|
|
2549
|
+
echoBranches.push(` if echo == ${rustStrLit(pn)} {\n let tv = ${own};\n return Ok(ScriptedWireValue::new(Value::Obj(vec![(${rustStrLit(field.name)}.to_string(), ${serializeTyped("tv", field.type, plan)})])));\n }`);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
else if (ref.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items")) {
|
|
2554
|
+
// #129: an arr node with an `items` port echoes the NATIVE port array (serialized element-by-element
|
|
2555
|
+
// into a Value::Arr) so the node→leaf array dataflow test is non-vacuous.
|
|
2556
|
+
const elemRef = ref.elem;
|
|
2557
|
+
echoBranches.push(` if echo == "items" {\n let tv = Value::Arr(ports.${portFieldName("items")}.iter().map(|e| { let ev = e.clone(); ${serializeTyped("ev", elemRef, plan)} }).collect());\n return Ok(ScriptedWireValue::new(tv));\n }`);
|
|
2558
|
+
}
|
|
2559
|
+
const canEcho = echoBranches.length > 0;
|
|
2560
|
+
const portsParam = canEcho ? "ports" : "_ports";
|
|
2561
|
+
const okBind = canEcho ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
|
|
2562
|
+
const echoBlock = canEcho ? `${echoBranches.join("\n")}\n` : "";
|
|
2563
|
+
methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Result<ScriptedWireValue, BehaviorError> {
|
|
2564
|
+
let ${okBind} = self.next(${rustStrLit(node.component)}).ok_or_else(|| unknown_component(${rustStrLit(node.component)}))?;
|
|
2099
2565
|
if is_err {
|
|
2100
|
-
return
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
Some(${decode}(&v))
|
|
2566
|
+
return Err(leaf_failure(err));
|
|
2567
|
+
}
|
|
2568
|
+
${echoBlock} Ok(ScriptedWireValue::new(ok.unwrap_or(Value::Null)))
|
|
2104
2569
|
}`);
|
|
2105
2570
|
}
|
|
2106
|
-
|
|
2571
|
+
const cHasHandler = c.body.some((n) => !("cond" in n));
|
|
2572
|
+
const wireAssoc = cHasHandler ? ` type Wire = ScriptedWireValue;\n` : "";
|
|
2573
|
+
traitImpls.push(`impl ${handlerTraitName(c.name)} for ScriptedNativeRaw {\n${wireAssoc}${methods.join("\n")}\n}`);
|
|
2107
2574
|
}
|
|
2108
2575
|
const arms = native
|
|
2109
2576
|
.map((c) => {
|
|
@@ -2152,7 +2619,7 @@ impl ScriptedNativeRaw {
|
|
|
2152
2619
|
}
|
|
2153
2620
|
|
|
2154
2621
|
// drain one scripted outcome for a component (last entry repeats — mirrors the reference scripted
|
|
2155
|
-
// handlers); None when the component has no scripted queue
|
|
2622
|
+
// handlers); None when the component has no scripted queue. Returns (is_error, err,
|
|
2156
2623
|
// ok_value, echo). The bc#87 parallel-stage runner calls node_* from multiple threads, so the drain
|
|
2157
2624
|
// is Mutex-guarded. #129: \`echo\` carries the reference scriptedHandlers' \`echo\` directive (e.g.
|
|
2158
2625
|
// "items") so a scripted node can ECHO a named input PORT back — used to make the node→leaf array
|
|
@@ -2162,13 +2629,11 @@ impl ScriptedNativeRaw {
|
|
|
2162
2629
|
let mut queues = self.queues.lock().unwrap();
|
|
2163
2630
|
let (queue, single) = queues.get_mut(component)?;
|
|
2164
2631
|
let raw = if *single || queue.len() <= 1 { queue[0].clone() } else { queue.remove(0) };
|
|
2165
|
-
//
|
|
2166
|
-
//
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
Some(other) => other.to_string(),
|
|
2171
|
-
None => String::new(),
|
|
2632
|
+
// normalize echo:port to the port NAME X so a de-box handler's echo comparison fires;
|
|
2633
|
+
// echo:items already names the port directly.
|
|
2634
|
+
let echo = {
|
|
2635
|
+
let e = raw.get("echo").and_then(|e| e.as_str()).unwrap_or("");
|
|
2636
|
+
if e == "port" { raw.get("port").and_then(|p| p.as_str()).unwrap_or("").to_string() } else { e.to_string() }
|
|
2172
2637
|
};
|
|
2173
2638
|
if let Some(okv) = raw.get("ok") {
|
|
2174
2639
|
let v = behavior_contracts::decode_value(okv).expect("decode ok");
|
|
@@ -2196,9 +2661,27 @@ use behavior_contracts::Value;
|
|
|
2196
2661
|
use super::*;
|
|
2197
2662
|
|
|
2198
2663
|
// bridge the covered module's LOCAL BehaviorError (super::BehaviorError) to the bc-runtime failure type
|
|
2199
|
-
// the conformance harness reads (same code + message) — the ONLY place the two meet
|
|
2664
|
+
// the conformance harness reads (same code + message + structured detail) — the ONLY place the two meet
|
|
2665
|
+
// (test glue). The de-box's structured Error Value is carried over so the harness can assert it.
|
|
2200
2666
|
fn to_rt_err(e: BehaviorError) -> behavior_contracts::BehaviorError {
|
|
2201
|
-
behavior_contracts::BehaviorError::new(e.code, e.message)
|
|
2667
|
+
let mut rt = behavior_contracts::BehaviorError::new(e.code, e.message);
|
|
2668
|
+
if let Some(d) = e.detail {
|
|
2669
|
+
let d = *d;
|
|
2670
|
+
rt.detail = Some(Box::new(behavior_contracts::ErrorDetail {
|
|
2671
|
+
kind: d.kind.map(|k| match k {
|
|
2672
|
+
ErrorKind::TypeMismatch => behavior_contracts::ErrorKind::TypeMismatch,
|
|
2673
|
+
ErrorKind::MissingField => behavior_contracts::ErrorKind::MissingField,
|
|
2674
|
+
ErrorKind::Overflow => behavior_contracts::ErrorKind::Overflow,
|
|
2675
|
+
}),
|
|
2676
|
+
model: d.model,
|
|
2677
|
+
field: d.field,
|
|
2678
|
+
expected_type: d.expected_type,
|
|
2679
|
+
actual_wire_type: d.actual_wire_type,
|
|
2680
|
+
raw_value: d.raw_value,
|
|
2681
|
+
context: d.context,
|
|
2682
|
+
}));
|
|
2683
|
+
}
|
|
2684
|
+
rt
|
|
2202
2685
|
}
|
|
2203
2686
|
${anyAsyncRunner
|
|
2204
2687
|
? `// bc#97 (TEST glue) — a minimal std-only block-on that drives an async runner Future to completion so
|
|
@@ -2232,12 +2715,12 @@ ${marshallers}
|
|
|
2232
2715
|
|
|
2233
2716
|
${emitMarshalHelpers()}
|
|
2234
2717
|
|
|
2235
|
-
// per-node concrete-row decoders (scripted Value -> RawRowNR_/RawElemNR_ struct).
|
|
2236
|
-
${decodeFns.join("\n\n")}
|
|
2237
|
-
|
|
2238
2718
|
// input decoders (generic &[(String,Value)] -> concrete InNR_<comp>; TEST glue, one decode at the boundary).
|
|
2239
2719
|
${inDecoders}
|
|
2240
2720
|
|
|
2721
|
+
// scripted wire adapter (TEST glue) — wraps a scripted Value as a WireValue the generated inline de-box probes.
|
|
2722
|
+
${wireAdapter}
|
|
2723
|
+
|
|
2241
2724
|
${adapter}
|
|
2242
2725
|
|
|
2243
2726
|
// scripted handler trait impls (one per covered component — the concrete node_* seam, test glue).
|
|
@@ -2260,10 +2743,8 @@ ${arms}
|
|
|
2260
2743
|
}
|
|
2261
2744
|
/** emitInDecoder — a TEST-glue decoder `decode_InNR_<comp>(input) -> Result<InNR_<comp>, BehaviorError>`.
|
|
2262
2745
|
* Reads each declared input port from the generic &[(String,Value)] into the concrete field (scalar →
|
|
2263
|
-
* typed read;
|
|
2264
|
-
*
|
|
2265
|
-
* same value `refCore` observes for the `null` binding run_behavior is given. Lives ONLY in the observe
|
|
2266
|
-
* companion. */
|
|
2746
|
+
* typed read; Value-typed → the raw Value clone). A missing key leaves Default (every covered input is
|
|
2747
|
+
* required + supplied by the corpus). Lives ONLY in the observe companion. */
|
|
2267
2748
|
function emitInDecoder(comp, plan) {
|
|
2268
2749
|
const name = inStructName(comp.name);
|
|
2269
2750
|
const ports = Object.keys(comp.inputPorts ?? {});
|
|
@@ -2285,15 +2766,7 @@ function emitInDecoder(comp, plan) {
|
|
|
2285
2766
|
const et = schema.elemType;
|
|
2286
2767
|
const head = i === 0 ? " if" : " } else if";
|
|
2287
2768
|
lines.push(`${head} k == ${rustStrLit(p)} {`);
|
|
2288
|
-
if (
|
|
2289
|
-
// An OPTIONAL port decodes into its native Option<T>. materializeExpr's opt de-box IS the wire
|
|
2290
|
-
// rule: `Value::Null` → None (absent), any other value → Some(<inner>). A key that never appears
|
|
2291
|
-
// keeps the struct Default (None) — the same absent value. Checked FIRST: optionality wraps every
|
|
2292
|
-
// inner type (an optional array is Option<Vec<T>>, not Vec<T>).
|
|
2293
|
-
const optRef = inputPortTypeRef(schema, plan);
|
|
2294
|
-
lines.push(` in_.${field} = ${materializeExpr("v", optRef, plan)};`);
|
|
2295
|
-
}
|
|
2296
|
-
else if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
2769
|
+
if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
2297
2770
|
// #108: decode the boxed Value::Arr into the native Vec<ElemT> (test glue — materialize each elem).
|
|
2298
2771
|
const elemRef = deriveTypeRef(et, plan);
|
|
2299
2772
|
const elemTy = renderTypeRef(elemRef);
|
|
@@ -2313,6 +2786,11 @@ function emitInDecoder(comp, plan) {
|
|
|
2313
2786
|
const mapRef = { kind: "map", value: deriveTypeRef(et, plan) };
|
|
2314
2787
|
lines.push(` in_.${field} = ${materializeExpr("v", mapRef, plan)};`);
|
|
2315
2788
|
}
|
|
2789
|
+
else if (inputPortIsOptional(schema)) {
|
|
2790
|
+
// #139: an OPTIONAL scalar port decodes into Option<T> via the inputPortTypeRef SSoT (a present key
|
|
2791
|
+
// materializes Some(v); an absent key never enters this block, leaving the Default None).
|
|
2792
|
+
lines.push(` in_.${field} = ${materializeExpr("v", inputPortTypeRef(schema, plan), plan)};`);
|
|
2793
|
+
}
|
|
2316
2794
|
else if (scalar === undefined) {
|
|
2317
2795
|
lines.push(` in_.${field} = v.clone();`);
|
|
2318
2796
|
}
|
|
@@ -2331,39 +2809,6 @@ function emitInDecoder(comp, plan) {
|
|
|
2331
2809
|
lines.push(`}`);
|
|
2332
2810
|
return lines.join("\n");
|
|
2333
2811
|
}
|
|
2334
|
-
/** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
|
|
2335
|
-
* declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row (a scalar/arr/
|
|
2336
|
-
* opt row via its `val`; a NAMED row via its flattened fields copied from the struct port). A port with a
|
|
2337
|
-
* different type is not echoable (the row could not hold it). */
|
|
2338
|
-
function echoablePorts(comp, node, ref, plan, typedNodes) {
|
|
2339
|
-
const want = renderTypeRef(ref);
|
|
2340
|
-
return Object.keys(node.ports).filter((p) => portFieldRustType(node.ports[p], comp, plan, typedNodes, `port '${p}'`) === want);
|
|
2341
|
-
}
|
|
2342
|
-
/** emitDecodeRow — emit (once, deduped) a decoder `decode_row_<comp>_<node><suffix>(v: &Value) -> <rowT>`
|
|
2343
|
-
* that materializes the scripted ok Value into the concrete row struct. Named: reuse the Value->struct
|
|
2344
|
-
* marshaller + move fields; scalar/arr/opt: materialize into .val. TEST glue only. */
|
|
2345
|
-
function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix) {
|
|
2346
|
-
const fn = `decode_row_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}${suffix.toLowerCase()}`;
|
|
2347
|
-
if (emitted.has(fn))
|
|
2348
|
-
return fn;
|
|
2349
|
-
emitted.add(fn);
|
|
2350
|
-
const lines = [];
|
|
2351
|
-
lines.push(`fn ${fn}(v: &Value) -> ${rowT} {`);
|
|
2352
|
-
if (ref.kind === "named") {
|
|
2353
|
-
const decl = findDecl(plan, ref.name);
|
|
2354
|
-
lines.push(` let t = marshal_${ref.name}(v).expect("scripted row decode");`);
|
|
2355
|
-
const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: t.${rustFieldName(f.name)}`).join(", ");
|
|
2356
|
-
lines.push(` ${rowT} { ${inits}, ..Default::default() }`);
|
|
2357
|
-
}
|
|
2358
|
-
else {
|
|
2359
|
-
const mat = materializeExpr("v", ref, plan);
|
|
2360
|
-
lines.push(` let val = (|| -> Result<${renderTypeRef(ref)}, BehaviorError> { Ok(${mat}) })().expect("scripted row decode");`);
|
|
2361
|
-
lines.push(` ${rowT} { val, ..Default::default() }`);
|
|
2362
|
-
}
|
|
2363
|
-
lines.push(`}`);
|
|
2364
|
-
out.push(lines.join("\n"));
|
|
2365
|
-
return fn;
|
|
2366
|
-
}
|
|
2367
2812
|
export const rustTypedNativeEmitter = {
|
|
2368
2813
|
language: "rust-typed-native",
|
|
2369
2814
|
classification: "codegen", // native: 型付きフラットコード生成・IR/dict 非在(#128/A6)
|