behavior-contracts 0.8.5 → 0.8.6
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/emit-typed-native-go.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-go.js +1332 -261
- package/dist/generator/emit-typed-native-go.js.map +1 -1
- package/dist/generator/emit-typed-native-rust.d.ts +3 -2
- package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
- package/dist/generator/emit-typed-native-rust.js +1328 -352
- 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
|
@@ -20,25 +20,71 @@ function rustErr(code, msgExpr) {
|
|
|
20
20
|
* fail closed with ZERO bc-runtime import. Codes match run_behavior verbatim (byte-equal). `code()` /
|
|
21
21
|
* `failure_code()` expose the stable code without a bc-runtime type. */
|
|
22
22
|
function emitBehaviorErrorType() {
|
|
23
|
-
return `//
|
|
23
|
+
return `// ErrorKind — what went wrong (the closed set of scp-error.md). A concrete enum: the covered plane
|
|
24
|
+
// carries no strings-as-tags and no dynamic kind lookup.
|
|
25
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
26
|
+
pub enum ErrorKind {
|
|
27
|
+
TypeMismatch,
|
|
28
|
+
MissingField,
|
|
29
|
+
Overflow,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl ErrorKind {
|
|
33
|
+
pub fn as_str(self) -> &'static str {
|
|
34
|
+
match self {
|
|
35
|
+
ErrorKind::TypeMismatch => "typeMismatch",
|
|
36
|
+
ErrorKind::MissingField => "missingField",
|
|
37
|
+
ErrorKind::Overflow => "overflow",
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ErrorDetail — the structured, recoverable payload a failure carries (scp-error.md "The Error
|
|
43
|
+
// Value"). The LEAF produces it at the wire boundary (it is the only party holding both the declared
|
|
44
|
+
// type and the raw wire datum); the runner transports it verbatim. Concrete fields — no boxed Value,
|
|
45
|
+
// no serialized blob in a string: \`expected_type\` is Portable Type Notation, a rendering of a
|
|
46
|
+
// STATICALLY DECLARED type, so nothing walks a type at runtime.
|
|
47
|
+
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
|
48
|
+
pub struct ErrorDetail {
|
|
49
|
+
pub kind: Option<ErrorKind>,
|
|
50
|
+
pub model: Option<String>,
|
|
51
|
+
pub field: Option<String>,
|
|
52
|
+
pub expected_type: Option<String>,
|
|
53
|
+
pub actual_wire_type: Option<String>,
|
|
54
|
+
pub raw_value: Option<String>,
|
|
55
|
+
pub context: std::collections::BTreeMap<String, String>,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
|
|
24
59
|
// covered runner returns \`Result<T, BehaviorError>\` over THIS local type instead of a bc-runtime
|
|
25
60
|
// failure, so the fully-covered module imports ZERO bc runtime. Codes match run_behavior verbatim
|
|
26
|
-
// (byte-equal).
|
|
27
|
-
//
|
|
61
|
+
// (byte-equal). \`detail\` carries the leaf's structured Error Value across the seam so a caller can
|
|
62
|
+
// log / skip / repair / migrate rather than parse a message. The observe companion (test glue, a
|
|
63
|
+
// super:: submodule) bridges this local error to the bc-runtime failure at the observe boundary —
|
|
64
|
+
// the covered module itself stays runtime-free.
|
|
28
65
|
#[derive(Debug, Clone)]
|
|
29
66
|
pub struct BehaviorError {
|
|
30
67
|
pub code: String,
|
|
31
68
|
pub message: String,
|
|
69
|
+
pub detail: Option<Box<ErrorDetail>>,
|
|
32
70
|
}
|
|
33
71
|
|
|
34
72
|
impl BehaviorError {
|
|
35
73
|
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
|
|
36
|
-
BehaviorError { code: code.into(), message: message.into() }
|
|
74
|
+
BehaviorError { code: code.into(), message: message.into(), detail: None }
|
|
75
|
+
}
|
|
76
|
+
/// The same failure carrying the leaf's structured Error Value.
|
|
77
|
+
pub fn with_detail(code: impl Into<String>, message: impl Into<String>, detail: ErrorDetail) -> Self {
|
|
78
|
+
BehaviorError { code: code.into(), message: message.into(), detail: Some(Box::new(detail)) }
|
|
37
79
|
}
|
|
38
80
|
/// The stable failure code (byte-equal to run_behavior) WITHOUT a bc-runtime type.
|
|
39
81
|
pub fn code(&self) -> &str {
|
|
40
82
|
&self.code
|
|
41
83
|
}
|
|
84
|
+
/// The structured payload, if this failure is about a datum.
|
|
85
|
+
pub fn detail(&self) -> Option<&ErrorDetail> {
|
|
86
|
+
self.detail.as_deref()
|
|
87
|
+
}
|
|
42
88
|
}
|
|
43
89
|
|
|
44
90
|
impl std::fmt::Display for BehaviorError {
|
|
@@ -46,7 +92,75 @@ impl std::fmt::Display for BehaviorError {
|
|
|
46
92
|
write!(f, "{}: {}", self.code, self.message)
|
|
47
93
|
}
|
|
48
94
|
}
|
|
49
|
-
impl std::error::Error for BehaviorError {}
|
|
95
|
+
impl std::error::Error for BehaviorError {}
|
|
96
|
+
|
|
97
|
+
// op_failed — wrap a leaf failure as the node's Failure under its Error Policy Kind, carrying the
|
|
98
|
+
// leaf's structured detail through unchanged (the runner transports the Error Value; it does not
|
|
99
|
+
// synthesize or re-interpret it).
|
|
100
|
+
#[allow(dead_code)]
|
|
101
|
+
fn op_failed(node: &str, policy: &str, e: BehaviorError) -> BehaviorError {
|
|
102
|
+
BehaviorError {
|
|
103
|
+
code: "OP_FAILED".to_string(),
|
|
104
|
+
message: format!("operation '{node}' failed under '{policy}' policy: {}", e.message),
|
|
105
|
+
detail: e.detail,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// de_type_mismatch / de_missing_field / de_overflow — the de-box mismatch failures the generated decode
|
|
110
|
+
// produces (scp-error.md "The Error Value"). Codes are byte-equal to the runtime outType check
|
|
111
|
+
// (TYPE_MISMATCH / MISSING_PROP) so the covered read stays equivalent to run_behavior; detail.kind
|
|
112
|
+
// distinguishes typeMismatch / missingField / overflow.
|
|
113
|
+
#[allow(dead_code)]
|
|
114
|
+
fn de_type_mismatch(model: &str, field: &str, expected: &str, actual_wire: String, raw: String) -> BehaviorError {
|
|
115
|
+
BehaviorError::with_detail(
|
|
116
|
+
"TYPE_MISMATCH",
|
|
117
|
+
format!("node '{model}': {field}: expected {expected}, got {actual_wire}"),
|
|
118
|
+
ErrorDetail {
|
|
119
|
+
kind: Some(ErrorKind::TypeMismatch),
|
|
120
|
+
model: Some(model.to_string()),
|
|
121
|
+
field: Some(field.to_string()),
|
|
122
|
+
expected_type: Some(expected.to_string()),
|
|
123
|
+
actual_wire_type: Some(actual_wire),
|
|
124
|
+
raw_value: Some(raw),
|
|
125
|
+
context: std::collections::BTreeMap::new(),
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[allow(dead_code)]
|
|
131
|
+
fn de_missing_field(model: &str, field: &str, expected: &str) -> BehaviorError {
|
|
132
|
+
BehaviorError::with_detail(
|
|
133
|
+
"MISSING_PROP",
|
|
134
|
+
format!("node '{model}': {field}: required field is absent"),
|
|
135
|
+
ErrorDetail {
|
|
136
|
+
kind: Some(ErrorKind::MissingField),
|
|
137
|
+
model: Some(model.to_string()),
|
|
138
|
+
field: Some(field.to_string()),
|
|
139
|
+
expected_type: Some(expected.to_string()),
|
|
140
|
+
actual_wire_type: None,
|
|
141
|
+
raw_value: None,
|
|
142
|
+
context: std::collections::BTreeMap::new(),
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[allow(dead_code)]
|
|
148
|
+
fn de_overflow(model: &str, field: &str, expected: &str, actual_wire: String, raw: String) -> BehaviorError {
|
|
149
|
+
let message = format!("node '{model}': {field}: {raw} is outside the range of {expected}");
|
|
150
|
+
BehaviorError::with_detail(
|
|
151
|
+
"TYPE_MISMATCH",
|
|
152
|
+
message,
|
|
153
|
+
ErrorDetail {
|
|
154
|
+
kind: Some(ErrorKind::Overflow),
|
|
155
|
+
model: Some(model.to_string()),
|
|
156
|
+
field: Some(field.to_string()),
|
|
157
|
+
expected_type: Some(expected.to_string()),
|
|
158
|
+
actual_wire_type: Some(actual_wire),
|
|
159
|
+
raw_value: Some(raw),
|
|
160
|
+
context: std::collections::BTreeMap::new(),
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
}`;
|
|
50
164
|
}
|
|
51
165
|
function portFieldName(wire) {
|
|
52
166
|
const safe = wire.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase();
|
|
@@ -78,6 +192,431 @@ function nodeMethodName(nodeId) {
|
|
|
78
192
|
function inStructName(compName) {
|
|
79
193
|
return `InNR${pascal(compName)}`;
|
|
80
194
|
}
|
|
195
|
+
function recordTop(ref) {
|
|
196
|
+
if (ref.kind === "named")
|
|
197
|
+
return { kind: "named", innerName: ref.name };
|
|
198
|
+
if (ref.kind === "arr" && ref.elem.kind === "named")
|
|
199
|
+
return { kind: "arr", innerName: ref.elem.name };
|
|
200
|
+
if (ref.kind === "opt" && ref.inner.kind === "named")
|
|
201
|
+
return { kind: "opt", innerName: ref.inner.name };
|
|
202
|
+
if (ref.kind === "map" && ref.value.kind === "named")
|
|
203
|
+
return { kind: "map", innerName: ref.value.name };
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
/** deBoxEligible — a componentRef node whose result is a record-containing top (named / arr(named) /
|
|
207
|
+
* opt(named) / map(named)). Its handler returns the wire; the generated decode probes each record. */
|
|
208
|
+
function deBoxEligible(ref) {
|
|
209
|
+
return recordTop(ref) !== null;
|
|
210
|
+
}
|
|
211
|
+
/** rustWireTypeForTop — the Rust handler return type (over the associated Self::Wire) for a record top. */
|
|
212
|
+
function rustWireTypeForTop(top) {
|
|
213
|
+
switch (top.kind) {
|
|
214
|
+
case "named":
|
|
215
|
+
return "Self::Wire";
|
|
216
|
+
case "arr":
|
|
217
|
+
return "Vec<Self::Wire>";
|
|
218
|
+
case "opt":
|
|
219
|
+
return "Option<Self::Wire>";
|
|
220
|
+
case "map":
|
|
221
|
+
return "std::collections::BTreeMap<String, Self::Wire>";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/** notationOfRef — the Portable Type Notation of a TypeRef (scp-error.md), resolving a named ref to its
|
|
225
|
+
* obj notation via the plan. Baked as the `expectedType` literal (a rendering of the STATICALLY declared
|
|
226
|
+
* type — nothing walks a type at runtime). */
|
|
227
|
+
function notationOfRef(ref, plan) {
|
|
228
|
+
switch (ref.kind) {
|
|
229
|
+
case "scalar":
|
|
230
|
+
return ref.scalar;
|
|
231
|
+
case "opt":
|
|
232
|
+
return `opt(${notationOfRef(ref.inner, plan)})`;
|
|
233
|
+
case "arr":
|
|
234
|
+
return `arr(${notationOfRef(ref.elem, plan)})`;
|
|
235
|
+
case "map":
|
|
236
|
+
return `map(${notationOfRef(ref.value, plan)})`;
|
|
237
|
+
case "named": {
|
|
238
|
+
const decl = findDecl(plan, ref.name);
|
|
239
|
+
return `obj{${decl.fields.map((f) => `${f.name}:${notationOfRef(f.type, plan)}`).join(",")}}`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** emitRustProbeWireTypes — the once-per-module probe enums + WireRow/WireList seam traits. Concrete
|
|
244
|
+
* enums; monomorphized traits (mutual Row/List associated types keep nesting concrete, no dyn/Box). */
|
|
245
|
+
function emitRustProbeWireTypes() {
|
|
246
|
+
return `// Probe<T> / NumProbe — the outcome of classifying one wire attribute against a declared type. Got carries
|
|
247
|
+
// the matched value (a NumProbe's raw numeric text, which the de-box parses + range-checks so overflow is
|
|
248
|
+
// BC's to detect); actual_wire_type is the producer's own wire tag (a free string, e.g. a DynamoDB
|
|
249
|
+
// "S"/"N"/"BOOL"); raw_value is the offending value stringified. Concrete enums — no boxed Value.
|
|
250
|
+
pub enum Probe<T> {
|
|
251
|
+
Got(T),
|
|
252
|
+
Wrong { actual_wire_type: String, raw_value: String },
|
|
253
|
+
Null { actual_wire_type: String, raw_value: String },
|
|
254
|
+
Absent,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
pub enum NumProbe {
|
|
258
|
+
Got { raw: String, actual_wire_type: String },
|
|
259
|
+
Wrong { actual_wire_type: String, raw_value: String },
|
|
260
|
+
Null { actual_wire_type: String, raw_value: String },
|
|
261
|
+
Absent,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// WireRow / WireList — the consumer's wire item / wire array, probed per declared field / element by the
|
|
265
|
+
// generated de-box. The consumer implements them over its wire payload (a DynamoDB AttributeValue map)
|
|
266
|
+
// and classifies each attribute's variant; the de-box owns strictness. Monomorphized (Row/List mutual
|
|
267
|
+
// associated types) — no dyn, no Box, no boxed Value crosses the seam.
|
|
268
|
+
pub trait WireRow: Sized {
|
|
269
|
+
type List: WireList<Row = Self>;
|
|
270
|
+
// the attribute keys present (the entries of a wire map / DynamoDB "M") — iterated to decode a map(V).
|
|
271
|
+
fn keys(&self) -> Vec<String>;
|
|
272
|
+
fn probe_string(&self, field: &str) -> Probe<String>;
|
|
273
|
+
fn probe_number(&self, field: &str) -> NumProbe;
|
|
274
|
+
fn probe_bool(&self, field: &str) -> Probe<bool>;
|
|
275
|
+
fn probe_row(&self, field: &str) -> Probe<Self>;
|
|
276
|
+
fn probe_list(&self, field: &str) -> Probe<Self::List>;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
pub trait WireList: Sized {
|
|
280
|
+
type Row: WireRow<List = Self>;
|
|
281
|
+
fn len(&self) -> usize;
|
|
282
|
+
fn is_empty(&self) -> bool {
|
|
283
|
+
self.len() == 0
|
|
284
|
+
}
|
|
285
|
+
fn elem_string(&self, i: usize) -> Probe<String>;
|
|
286
|
+
fn elem_number(&self, i: usize) -> NumProbe;
|
|
287
|
+
fn elem_bool(&self, i: usize) -> Probe<bool>;
|
|
288
|
+
fn elem_row(&self, i: usize) -> Probe<Self::Row>;
|
|
289
|
+
fn elem_list(&self, i: usize) -> Probe<Self>;
|
|
290
|
+
}`;
|
|
291
|
+
}
|
|
292
|
+
/** rustDecodeNamedName — the decode function name for a named type (deduped across the module). */
|
|
293
|
+
function rustDecodeNamedName(name) {
|
|
294
|
+
return `decode_wire_${name.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase()}`;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* emitRustDecodeNamed — emit (once, deduped) `decode_wire_<name><W: WireRow>(w: &W) -> Result<Name,
|
|
298
|
+
* BehaviorError>`: probe each declared field from the type SSoT (via the single rustDecodeValue
|
|
299
|
+
* recursion) and build the struct, failing closed with the structured Error Value on a mismatch.
|
|
300
|
+
*/
|
|
301
|
+
function emitRustDecodeNamed(name, plan, emitted, out) {
|
|
302
|
+
const fn = rustDecodeNamedName(name);
|
|
303
|
+
if (emitted.has(fn))
|
|
304
|
+
return fn;
|
|
305
|
+
emitted.add(fn);
|
|
306
|
+
const decl = findDecl(plan, name);
|
|
307
|
+
const lines = [];
|
|
308
|
+
lines.push(`// ${fn} — the strict de-box for '${name}': probe each declared field, fail closed on a`);
|
|
309
|
+
lines.push(`// wire mismatch with the structured Error Value (byte-equal codes to run_behavior).`);
|
|
310
|
+
lines.push(`fn ${fn}<W: WireRow>(w: &W) -> Result<${name}, BehaviorError> {`);
|
|
311
|
+
const ctr = { n: 0 };
|
|
312
|
+
const binds = [];
|
|
313
|
+
for (const f of decl.fields) {
|
|
314
|
+
const rf = rustFieldName(f.name);
|
|
315
|
+
const expr = rustDecodeValue(f.type, "w", rustStrLit(f.name), "row", name, f.name, plan, emitted, out, ctr, 1);
|
|
316
|
+
lines.push(` let ${rf} = ${expr};`);
|
|
317
|
+
binds.push(rf);
|
|
318
|
+
}
|
|
319
|
+
lines.push(` Ok(${name} { ${binds.join(", ")} })`);
|
|
320
|
+
lines.push(`}`);
|
|
321
|
+
out.push(lines.join("\n"));
|
|
322
|
+
return fn;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* rustDecodeValue — the SINGLE recursive decode over the full TypeRef grammar (scalar / opt(scalar) /
|
|
326
|
+
* named / arr / map). Returns a Rust expression (a match block) that evaluates to the decoded value of
|
|
327
|
+
* `ref`, obtained by probing `srcExpr` at `keyExpr` (a WireRow field via `probe_*`, srcKind "row"; or a
|
|
328
|
+
* WireList element via `elem_*`, srcKind "list"). Every nested position (arr element, map value, obj
|
|
329
|
+
* field) recurses through THIS function, so arbitrary nesting works by construction. A mismatch does
|
|
330
|
+
* `return Err(<structured error>)`; no panic, no boxed value.
|
|
331
|
+
*/
|
|
332
|
+
function rustDecodeValue(ref, srcExpr, keyExpr, srcKind, model, fieldRaw, plan, emitted, out, ctr, indent) {
|
|
333
|
+
const I = " ".repeat(indent);
|
|
334
|
+
const I1 = " ".repeat(indent + 1);
|
|
335
|
+
const ml = rustStrLit(model);
|
|
336
|
+
const fl = rustStrLit(fieldRaw);
|
|
337
|
+
const nl = rustStrLit(notationOfRef(ref, plan));
|
|
338
|
+
const id = ctr.n++;
|
|
339
|
+
const prefix = srcKind === "row" ? "probe_" : "elem_";
|
|
340
|
+
const mismatch = `return Err(de_type_mismatch(${ml}, ${fl}, ${nl}, actual_wire_type, raw_value))`;
|
|
341
|
+
const missing = `return Err(de_missing_field(${ml}, ${fl}, ${nl}))`;
|
|
342
|
+
if (ref.kind === "opt" && ref.inner.kind === "scalar" && ref.inner.scalar !== "null") {
|
|
343
|
+
const inner = ref.inner.scalar;
|
|
344
|
+
if (inner === "int" || inner === "float") {
|
|
345
|
+
const rustT = inner === "int" ? "i64" : "f64";
|
|
346
|
+
return `match ${srcExpr}.${prefix}number(${keyExpr}) {
|
|
347
|
+
${I1}NumProbe::Got { raw, actual_wire_type } => match raw.parse::<${rustT}>() {
|
|
348
|
+
${I1} Ok(n) => Some(n),
|
|
349
|
+
${I1} Err(_) => return Err(de_overflow(${ml}, ${fl}, ${nl}, actual_wire_type, raw)),
|
|
350
|
+
${I1}},
|
|
351
|
+
${I1}NumProbe::Wrong { actual_wire_type, raw_value } => ${mismatch},
|
|
352
|
+
${I1}NumProbe::Null { .. } | NumProbe::Absent => None,
|
|
353
|
+
${I}}`;
|
|
354
|
+
}
|
|
355
|
+
const meth = inner === "string" ? "string" : "bool";
|
|
356
|
+
return `match ${srcExpr}.${prefix}${meth}(${keyExpr}) {
|
|
357
|
+
${I1}Probe::Got(v) => Some(v),
|
|
358
|
+
${I1}Probe::Wrong { actual_wire_type, raw_value } => ${mismatch},
|
|
359
|
+
${I1}Probe::Null { .. } | Probe::Absent => None,
|
|
360
|
+
${I}}`;
|
|
361
|
+
}
|
|
362
|
+
if (ref.kind === "scalar" && ref.scalar !== "null") {
|
|
363
|
+
if (ref.scalar === "int" || ref.scalar === "float") {
|
|
364
|
+
const rustT = ref.scalar === "int" ? "i64" : "f64";
|
|
365
|
+
return `match ${srcExpr}.${prefix}number(${keyExpr}) {
|
|
366
|
+
${I1}NumProbe::Got { raw, actual_wire_type } => match raw.parse::<${rustT}>() {
|
|
367
|
+
${I1} Ok(n) => n,
|
|
368
|
+
${I1} Err(_) => return Err(de_overflow(${ml}, ${fl}, ${nl}, actual_wire_type, raw)),
|
|
369
|
+
${I1}},
|
|
370
|
+
${I1}NumProbe::Wrong { actual_wire_type, raw_value }
|
|
371
|
+
${I1}| NumProbe::Null { actual_wire_type, raw_value } => ${mismatch},
|
|
372
|
+
${I1}NumProbe::Absent => ${missing},
|
|
373
|
+
${I}}`;
|
|
374
|
+
}
|
|
375
|
+
const meth = ref.scalar === "string" ? "string" : "bool";
|
|
376
|
+
return `match ${srcExpr}.${prefix}${meth}(${keyExpr}) {
|
|
377
|
+
${I1}Probe::Got(v) => v,
|
|
378
|
+
${I1}Probe::Wrong { actual_wire_type, raw_value }
|
|
379
|
+
${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
|
|
380
|
+
${I1}Probe::Absent => ${missing},
|
|
381
|
+
${I}}`;
|
|
382
|
+
}
|
|
383
|
+
if (ref.kind === "named") {
|
|
384
|
+
const dec = emitRustDecodeNamed(ref.name, plan, emitted, out);
|
|
385
|
+
const rowMeth = srcKind === "row" ? "probe_row" : "elem_row";
|
|
386
|
+
return `match ${srcExpr}.${rowMeth}(${keyExpr}) {
|
|
387
|
+
${I1}Probe::Got(sub) => ${dec}(&sub)?,
|
|
388
|
+
${I1}Probe::Wrong { actual_wire_type, raw_value }
|
|
389
|
+
${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
|
|
390
|
+
${I1}Probe::Absent => ${missing},
|
|
391
|
+
${I}}`;
|
|
392
|
+
}
|
|
393
|
+
if (ref.kind === "arr") {
|
|
394
|
+
const listMeth = srcKind === "row" ? "probe_list" : "elem_list";
|
|
395
|
+
const elemExpr = rustDecodeValue(ref.elem, `l${id}`, `i${id}`, "list", model, fieldRaw, plan, emitted, out, ctr, indent + 2);
|
|
396
|
+
return `match ${srcExpr}.${listMeth}(${keyExpr}) {
|
|
397
|
+
${I1}Probe::Got(l${id}) => {
|
|
398
|
+
${I1} let mut acc = Vec::with_capacity(l${id}.len());
|
|
399
|
+
${I1} for i${id} in 0..l${id}.len() {
|
|
400
|
+
${I1} acc.push(${elemExpr});
|
|
401
|
+
${I1} }
|
|
402
|
+
${I1} acc
|
|
403
|
+
${I1}}
|
|
404
|
+
${I1}Probe::Wrong { actual_wire_type, raw_value }
|
|
405
|
+
${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
|
|
406
|
+
${I1}Probe::Absent => ${missing},
|
|
407
|
+
${I}}`;
|
|
408
|
+
}
|
|
409
|
+
if (ref.kind === "map") {
|
|
410
|
+
// a declared map(V): the wire value is a producer map (a DynamoDB "M"); enumerate its keys and decode
|
|
411
|
+
// each value as V through THIS same recursion (so map-of-map / map-of-arr nest by construction).
|
|
412
|
+
const rowMeth = srcKind === "row" ? "probe_row" : "elem_row";
|
|
413
|
+
const valExpr = rustDecodeValue(ref.value, `m${id}`, `&k${id}`, "row", model, fieldRaw, plan, emitted, out, ctr, indent + 2);
|
|
414
|
+
return `match ${srcExpr}.${rowMeth}(${keyExpr}) {
|
|
415
|
+
${I1}Probe::Got(m${id}) => {
|
|
416
|
+
${I1} let mut acc = std::collections::BTreeMap::new();
|
|
417
|
+
${I1} for k${id} in m${id}.keys() {
|
|
418
|
+
${I1} let v${id} = ${valExpr};
|
|
419
|
+
${I1} acc.insert(k${id}, v${id});
|
|
420
|
+
${I1} }
|
|
421
|
+
${I1} acc
|
|
422
|
+
${I1}}
|
|
423
|
+
${I1}Probe::Wrong { actual_wire_type, raw_value }
|
|
424
|
+
${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
|
|
425
|
+
${I1}Probe::Absent => ${missing},
|
|
426
|
+
${I}}`;
|
|
427
|
+
}
|
|
428
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: '${fieldRaw}' of '${model}' has type ${JSON.stringify(ref)} which the generated de-box does not decode (supported: scalar / opt(scalar) / named / arr / map).`);
|
|
429
|
+
}
|
|
430
|
+
/** rustDecodeElemName — the element de-box function name for a map/fanout node. */
|
|
431
|
+
function rustDecodeElemName(compName, nodeId) {
|
|
432
|
+
return `decode_wire_elem_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase()}`;
|
|
433
|
+
}
|
|
434
|
+
/** mapFanoutElemRef — the element ROW TypeRef a map/fanout node decodes (the per-element record). */
|
|
435
|
+
function mapFanoutElemRef(node, typedNodes, plan) {
|
|
436
|
+
const ref = typedNodes.get(node.id);
|
|
437
|
+
if ("fanout" in node)
|
|
438
|
+
return fanoutItemsElemRef(node, ref, plan);
|
|
439
|
+
return mapElemRowRef(node, ref, plan);
|
|
440
|
+
}
|
|
441
|
+
/** mapFanoutElemDeBoxEligible — a map/fanout node whose element ROW is a named record de-boxes each
|
|
442
|
+
* element through the generated decode_wire_elem_* (closing the scan-row silent-default hole). */
|
|
443
|
+
function mapFanoutElemDeBoxEligible(node, typedNodes, plan) {
|
|
444
|
+
return mapFanoutElemRef(node, typedNodes, plan).kind === "named";
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* emitRustDecodeElemRow — emit (once) `decode_wire_elem_<comp>_<node><W: WireRow>(w: &W) ->
|
|
448
|
+
* Result<RawElemNR<comp><node>, BehaviorError>` for a map/fanout node whose element ROW is a named
|
|
449
|
+
* record: probe each declared field via the SAME rustDecodeValue recursion as record reads and build the
|
|
450
|
+
* concrete RawElemNR struct, failing closed with the structured Error Value on a mismatch.
|
|
451
|
+
*/
|
|
452
|
+
function emitRustDecodeElemRow(comp, node, elemRowRef, plan, emitted, out) {
|
|
453
|
+
const nodeId = node.id;
|
|
454
|
+
const fn = rustDecodeElemName(comp.name, nodeId);
|
|
455
|
+
if (emitted.has(fn))
|
|
456
|
+
return fn;
|
|
457
|
+
emitted.add(fn);
|
|
458
|
+
const retType = rawElemStructName(comp.name, nodeId);
|
|
459
|
+
const decl = findDecl(plan, elemRowRef.name);
|
|
460
|
+
const lines = [];
|
|
461
|
+
lines.push(`// ${fn} — the strict de-box for a '${nodeId}' element row: probe each declared field, fail`);
|
|
462
|
+
lines.push(`// closed on a wire mismatch with the structured Error Value (byte-equal codes to run_behavior).`);
|
|
463
|
+
lines.push(`fn ${fn}<W: WireRow>(w: &W) -> Result<${retType}, BehaviorError> {`);
|
|
464
|
+
const ctr = { n: 0 };
|
|
465
|
+
const binds = [];
|
|
466
|
+
for (const f of decl.fields) {
|
|
467
|
+
const rf = rustFieldName(f.name);
|
|
468
|
+
const expr = rustDecodeValue(f.type, "w", rustStrLit(f.name), "row", elemRowRef.name, f.name, plan, emitted, out, ctr, 1);
|
|
469
|
+
lines.push(` let ${rf} = ${expr};`);
|
|
470
|
+
binds.push(rf);
|
|
471
|
+
}
|
|
472
|
+
lines.push(` Ok(${retType} { ${binds.join(", ")} })`);
|
|
473
|
+
lines.push(`}`);
|
|
474
|
+
out.push(lines.join("\n"));
|
|
475
|
+
return fn;
|
|
476
|
+
}
|
|
477
|
+
/** emitRustNodeDecodeFns — emit the decode functions for every de-box-eligible node: a componentRef
|
|
478
|
+
* record read (decode_wire_<name>) OR a map/fanout element row (decode_wire_elem_<comp>_<node>). */
|
|
479
|
+
/** rustDecodeResultName — the top-result decode function name for a de-box componentRef node. */
|
|
480
|
+
function rustDecodeResultName(compName, nodeId) {
|
|
481
|
+
return `decode_wire_result_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase()}`;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* emitRustDecodeWireResult — emit (once) `decode_wire_result_<comp>_<node><W: WireRow>(wire: <WireArg>)
|
|
485
|
+
* -> Result<<ResultType>, BehaviorError>` for a componentRef node whose result is a record-containing
|
|
486
|
+
* top: it decodes each record via decode_wire_<name> and assembles the top (record / Vec / Option /
|
|
487
|
+
* BTreeMap). One path covers named / arr(named) / opt(named) / map(named).
|
|
488
|
+
*/
|
|
489
|
+
function emitRustDecodeWireResult(comp, node, ref, plan, emitted, out) {
|
|
490
|
+
const top = recordTop(ref);
|
|
491
|
+
const fn = rustDecodeResultName(comp.name, node.id);
|
|
492
|
+
if (emitted.has(fn))
|
|
493
|
+
return fn;
|
|
494
|
+
emitted.add(fn);
|
|
495
|
+
const innerDecode = emitRustDecodeNamed(top.innerName, plan, emitted, out);
|
|
496
|
+
const resultType = renderTypeRef(ref);
|
|
497
|
+
const lines = [];
|
|
498
|
+
lines.push(`// ${fn} — decode the node's record-containing '${top.kind}' result from the wire (each record`);
|
|
499
|
+
lines.push(`// via ${innerDecode}); a de-box mismatch fails closed with the structured Error Value.`);
|
|
500
|
+
switch (top.kind) {
|
|
501
|
+
case "named":
|
|
502
|
+
lines.push(`fn ${fn}<W: WireRow>(wire: W) -> Result<${resultType}, BehaviorError> {`);
|
|
503
|
+
lines.push(` ${innerDecode}(&wire)`);
|
|
504
|
+
break;
|
|
505
|
+
case "arr":
|
|
506
|
+
lines.push(`fn ${fn}<W: WireRow>(wire: Vec<W>) -> Result<${resultType}, BehaviorError> {`);
|
|
507
|
+
lines.push(` let mut out = Vec::with_capacity(wire.len());`);
|
|
508
|
+
lines.push(` for w in wire {`);
|
|
509
|
+
lines.push(` out.push(${innerDecode}(&w)?);`);
|
|
510
|
+
lines.push(` }`);
|
|
511
|
+
lines.push(` Ok(out)`);
|
|
512
|
+
break;
|
|
513
|
+
case "opt":
|
|
514
|
+
lines.push(`fn ${fn}<W: WireRow>(wire: Option<W>) -> Result<${resultType}, BehaviorError> {`);
|
|
515
|
+
lines.push(` match wire {`);
|
|
516
|
+
lines.push(` Some(w) => Ok(Some(${innerDecode}(&w)?)),`);
|
|
517
|
+
lines.push(` None => Ok(None),`);
|
|
518
|
+
lines.push(` }`);
|
|
519
|
+
break;
|
|
520
|
+
case "map":
|
|
521
|
+
lines.push(`fn ${fn}<W: WireRow>(wire: std::collections::BTreeMap<String, W>) -> Result<${resultType}, BehaviorError> {`);
|
|
522
|
+
lines.push(` let mut out = std::collections::BTreeMap::new();`);
|
|
523
|
+
lines.push(` for (k, w) in wire {`);
|
|
524
|
+
lines.push(` out.insert(k, ${innerDecode}(&w)?);`);
|
|
525
|
+
lines.push(` }`);
|
|
526
|
+
lines.push(` Ok(out)`);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
lines.push(`}`);
|
|
530
|
+
out.push(lines.join("\n"));
|
|
531
|
+
return fn;
|
|
532
|
+
}
|
|
533
|
+
function emitRustNodeDecodeFns(comp, typedNodes, plan, emitted, out) {
|
|
534
|
+
for (const n of comp.body) {
|
|
535
|
+
if ("cond" in n)
|
|
536
|
+
continue;
|
|
537
|
+
if ("map" in n || "fanout" in n) {
|
|
538
|
+
const node = n;
|
|
539
|
+
if (!mapFanoutElemDeBoxEligible(node, typedNodes, plan))
|
|
540
|
+
continue;
|
|
541
|
+
emitRustDecodeElemRow(comp, node, mapFanoutElemRef(node, typedNodes, plan), plan, emitted, out);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const ref = typedNodes.get(n.id);
|
|
545
|
+
if (ref === undefined)
|
|
546
|
+
continue;
|
|
547
|
+
const top = recordTop(ref);
|
|
548
|
+
if (top === null)
|
|
549
|
+
continue;
|
|
550
|
+
// a NAMED top decodes in place via decode_wire_<name>; an arr/opt/map top assembles via
|
|
551
|
+
// decode_wire_result_* (which recurses into decode_wire_<name> per record).
|
|
552
|
+
if (top.kind === "named")
|
|
553
|
+
emitRustDecodeNamed(top.innerName, plan, emitted, out);
|
|
554
|
+
else
|
|
555
|
+
emitRustDecodeWireResult(comp, n, ref, plan, emitted, out);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/** containsNamed — whether a TypeRef transitively contains a named record (through arr / opt / map). */
|
|
559
|
+
function containsNamed(ref) {
|
|
560
|
+
switch (ref.kind) {
|
|
561
|
+
case "scalar":
|
|
562
|
+
return false;
|
|
563
|
+
case "named":
|
|
564
|
+
return true;
|
|
565
|
+
case "opt":
|
|
566
|
+
return containsNamed(ref.inner);
|
|
567
|
+
case "arr":
|
|
568
|
+
return containsNamed(ref.elem);
|
|
569
|
+
case "map":
|
|
570
|
+
return containsNamed(ref.value);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* assertDeBoxableResults — fail closed (never a silent trust) for a covered node whose de-boxable result
|
|
575
|
+
* is a NON-named top (arr / opt / map) that transitively contains a record: the generated de-box decodes
|
|
576
|
+
* records only under a NAMED top (decode_wire_* / decode_wire_elem_*), so a record inside a non-named top
|
|
577
|
+
* would be materialized on the trusting `.val` path and silently default its wire fields. That is the
|
|
578
|
+
* silent-default red line forbids, so it is a loud generation-time error (native-codegen-standard
|
|
579
|
+
* §2). A NAMED top (de-boxed) or a pure-scalar non-named top (no records to de-box) is fine.
|
|
580
|
+
*/
|
|
581
|
+
/** readsAPriorNode — a componentRef node is a DATAFLOW consumer iff at least one input port is a ref
|
|
582
|
+
* headed by a PRIOR BODY NODE (it reads a prior node's already-native result, #129), rather than a fresh
|
|
583
|
+
* external wire read whose ports come from input params / static / an element ($as) binding. Same
|
|
584
|
+
* head-is-a-body-node signal priorNodeArrElemInfo (#129) uses to lower a prior-node array feed. */
|
|
585
|
+
function readsAPriorNode(node, typedNodes) {
|
|
586
|
+
for (const expr of Object.values(node.ports ?? {})) {
|
|
587
|
+
const e = classifyExpr(expr);
|
|
588
|
+
if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0]))
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
function assertDeBoxableResults(comp, typedNodes, plan) {
|
|
594
|
+
for (const n of comp.body) {
|
|
595
|
+
if ("cond" in n)
|
|
596
|
+
continue;
|
|
597
|
+
const isMapFanout = "map" in n || "fanout" in n;
|
|
598
|
+
const ref = isMapFanout ? mapFanoutElemRef(n, typedNodes, plan) : typedNodes.get(n.id);
|
|
599
|
+
const deBoxed = isMapFanout ? ref.kind === "named" : recordTop(ref) !== null;
|
|
600
|
+
if (deBoxed)
|
|
601
|
+
continue;
|
|
602
|
+
const nodeId = n.id;
|
|
603
|
+
if (containsNamed(ref)) {
|
|
604
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: node '${nodeId}' shape ${JSON.stringify(ref)} contains a record the de-box does not decode (a deeper nesting than a direct named / arr / opt / map of a named record). It would trust the record's wire fields (silent default) — the silent-default red line forbids. Fail closed: reshape so the record is a direct de-box top, or extend the recursion for this nesting.`);
|
|
605
|
+
}
|
|
606
|
+
// a map/fanout element is ALWAYS a per-element wire read; only a named record is de-boxed
|
|
607
|
+
// (decode_wire_elem_*). A scalar / scalar-collection element materializes from the trusting per-element
|
|
608
|
+
// path — a wrong wire value silent-defaults. Fail closed.
|
|
609
|
+
if (isMapFanout) {
|
|
610
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: map/fanout node '${nodeId}' element ${JSON.stringify(ref)} is a scalar / scalar-collection per-element wire read that materializes on the trusting typed path — a wrong wire value would silent-default (diverging from run_behavior's outType check). Fail closed: declare the element as a record (de-boxed).`);
|
|
611
|
+
}
|
|
612
|
+
// a pure-scalar / scalar-collection top on a HANDLER-BACKED componentRef wire read trusts the typed
|
|
613
|
+
// materialization (a wrong wire value silent-defaults). A DATAFLOW consumer (a port fed by a prior
|
|
614
|
+
// node) computes from already-native data and generates.
|
|
615
|
+
if (!readsAPriorNode(n, typedNodes)) {
|
|
616
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: node '${nodeId}' result top ${JSON.stringify(ref)} is a handler-backed scalar / scalar-collection read that materializes on the trusting typed path — a wrong wire value would silent-default (diverging from run_behavior's outType check). Fail closed: declare the result as a record (de-boxed) or feed it as dataflow from a prior node.`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
81
620
|
function staticArrElems(node) {
|
|
82
621
|
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
83
622
|
return null;
|
|
@@ -88,11 +627,60 @@ function staticArrElems(node) {
|
|
|
88
627
|
return Array.isArray(arg) ? arg : null;
|
|
89
628
|
}
|
|
90
629
|
/**
|
|
91
|
-
* portFieldRustType — the NATIVE Rust type for a covered port field (bc#90 / runtime-free)
|
|
92
|
-
*
|
|
93
|
-
|
|
94
|
-
*
|
|
95
|
-
*
|
|
630
|
+
* portFieldRustType — the NATIVE Rust type for a covered port field (bc#90 / runtime-free). The covered
|
|
631
|
+
* ports struct carries NO `Value` field: every port lowers to a concrete Rust type.
|
|
632
|
+
/** rustOwn — the ownership renderer native-expr applies at a resolved ref LEAF: an array field is cloned
|
|
633
|
+
* (owned) when `ownArrays` (a port) or borrowed (a cond `len`); a Copy scalar behind a guard is deref'd
|
|
634
|
+
* (unless a field was already accessed by value); anything else clones. */
|
|
635
|
+
function rustOwn(guard, ownArrays) {
|
|
636
|
+
return (expr, ref, fieldAccessed) => {
|
|
637
|
+
if (ref.kind === "arr")
|
|
638
|
+
return ownArrays ? `${expr}.clone()` : expr;
|
|
639
|
+
if (typeRefIsCopy(ref))
|
|
640
|
+
return guard && !fieldAccessed ? `*${expr}` : expr;
|
|
641
|
+
return `${expr}.clone()`;
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
/** nativeResolveHead — the ONE ref-head resolver the runtime-free native-expr compiler reads (cond `if`/
|
|
645
|
+
* branches, a map/fanout `when` guard, AND a port). A head is a `$as` element binding, a prior body node's
|
|
646
|
+
* typed cell (behind a RefCell borrow guard), or an input port (a by-value struct field). #139: the input
|
|
647
|
+
* ref comes from the inputPortTypeRef SSoT so an OPTIONAL port resolves as `{opt:…}` (native Option<T>). */
|
|
648
|
+
function nativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
|
|
649
|
+
const ownArrays = opts?.ownArrays === true;
|
|
650
|
+
return (head) => {
|
|
651
|
+
if (opts?.asBinding !== undefined && opts.asBase !== undefined && head === opts.asBinding.name) {
|
|
652
|
+
return { expr: opts.asBase, ref: opts.asBinding.ref, own: rustOwn(true, ownArrays) };
|
|
653
|
+
}
|
|
654
|
+
if (isPrior(head)) {
|
|
655
|
+
const ref = typedNodes.get(head);
|
|
656
|
+
return { expr: `${typedCell(head)}.borrow()`, ref, own: rustOwn(true, ownArrays) };
|
|
657
|
+
}
|
|
658
|
+
const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
|
|
659
|
+
if (ref === undefined)
|
|
660
|
+
return null;
|
|
661
|
+
return { expr: `in_.${rustFieldName(head)}`, ref, own: rustOwn(false, ownArrays) };
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
/** compilePortNode — lower a port expression to its native ports-struct field via the runtime-free
|
|
665
|
+
* native-expr compiler: the ONE port lowering. The port's field TYPE is the compiled `ref`, its
|
|
666
|
+
* initializer the compiled `expr` — derived from a SINGLE lowering, so they cannot disagree. A port that
|
|
667
|
+
* cannot lower FAILS CLOSED; a FALLIBLE lowering (hoists statements) is returned as-is (only emitPortInit
|
|
668
|
+
* rejects it). Reuses the same nativeResolveHead cond/guard/port share, with `ownArrays` (a port owns). */
|
|
669
|
+
function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
|
|
670
|
+
const resolveHead = nativeResolveHead(comp, plan, typedNodes, isPrior, { ...opts, ownArrays: true });
|
|
671
|
+
try {
|
|
672
|
+
return new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compilePort(node);
|
|
673
|
+
}
|
|
674
|
+
catch (e) {
|
|
675
|
+
if (!(e instanceof GeneratorFailure))
|
|
676
|
+
throw e;
|
|
677
|
+
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}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* portFieldRustType — the NATIVE Rust type for a covered port field, read off the ONE port lowering
|
|
682
|
+
* (compilePortNode / native-expr). Field type = the compiled TypeRef, so it can never disagree with the
|
|
683
|
+
* initializer (emitPortInit), which reads the SAME lowering for its value. `where` names the site.
|
|
96
684
|
*/
|
|
97
685
|
function portFieldRustType(node, comp, plan, typedNodes, where = "port", opts) {
|
|
98
686
|
const c = compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), where, opts);
|
|
@@ -105,12 +693,17 @@ function portFieldRustType(node, comp, plan, typedNodes, where = "port", opts) {
|
|
|
105
693
|
* reads `ports.f_<field>` DIRECTLY. The struct derives Clone (a bc#87 parallel stage clones it into the
|
|
106
694
|
* per-worker dispatch). Every field is a NATIVE Rust type (bc#90 / runtime-free) — NO boxed `Value`.
|
|
107
695
|
*/
|
|
108
|
-
function emitPortsStruct(comp, node, plan, typedNodes
|
|
696
|
+
function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
|
|
109
697
|
const name = portsStructName(comp.name, node.id);
|
|
110
698
|
const portNames = Object.keys(node.ports);
|
|
111
699
|
if (portNames.length === 0) {
|
|
112
700
|
return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
|
|
113
701
|
}
|
|
702
|
+
// a node WITH ports always resolves through the one port lowering, which needs the type plan + the
|
|
703
|
+
// prior-node map (both are supplied by every caller — narrow the optional params for the fields below).
|
|
704
|
+
if (plan === undefined || typedNodes === undefined) {
|
|
705
|
+
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native: ports struct for node '${node.id}' needs a type plan + prior-node map`);
|
|
706
|
+
}
|
|
114
707
|
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
708
|
const fields = portNames
|
|
116
709
|
.map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
|
|
@@ -135,7 +728,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
|
|
|
135
728
|
// #108: element ports may read the `$as` binding — resolve the over element type for native typing.
|
|
136
729
|
const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
137
730
|
const asBinding = { name: m.as, ref: elemRef, plan };
|
|
138
|
-
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, plan, typedNodes
|
|
731
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan, typedNodes);
|
|
139
732
|
return `${elemStruct}
|
|
140
733
|
|
|
141
734
|
// ${batch} — CONCRETE batched ports for map '${node.id}': the Vec of per-element CONCRETE ports structs
|
|
@@ -153,7 +746,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
|
|
|
153
746
|
const batch = `${name}Batch`;
|
|
154
747
|
const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
|
|
155
748
|
const asBinding = { name: f.as, ref: elemRef, plan };
|
|
156
|
-
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, plan, typedNodes
|
|
749
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan, typedNodes);
|
|
157
750
|
return `${elemStruct}
|
|
158
751
|
|
|
159
752
|
// ${batch} — CONCRETE batched ports for fanout '${node.id}': the Vec of per-id CONCRETE ports structs.
|
|
@@ -173,6 +766,20 @@ let rustExprTempSeq = 0;
|
|
|
173
766
|
/** module-level accumulator: set when the native-expr compiler emits a fallible helper call (so emit()
|
|
174
767
|
* bakes the helper library). Reset at the start of each emit(). */
|
|
175
768
|
const rustExprUsed = { helpers: false };
|
|
769
|
+
/** rustBlockBody — the body of a block-expression that runs `stmts` then yields `expr`. A trailing
|
|
770
|
+
* `let t = E; t` collapses to `E` (clippy `let_and_return`). */
|
|
771
|
+
function rustBlockBody(stmts, expr) {
|
|
772
|
+
const last = stmts[stmts.length - 1];
|
|
773
|
+
const m = last !== undefined ? /^let\s+([A-Za-z0-9_]+)(?::\s*[^=]+)?\s*=\s*(.+);$/.exec(last) : null;
|
|
774
|
+
if (m !== null && m[1] === expr)
|
|
775
|
+
return [...stmts.slice(0, -1), m[2]].join(" ");
|
|
776
|
+
return [...stmts, rustStripOuterParens(expr)].join(" ");
|
|
777
|
+
}
|
|
778
|
+
/** Drop a TRAILING `.clone()` — for a position that only reads the value (a presence test), so an owning
|
|
779
|
+
* clone added by a head resolver is pure waste. Only a clone at the very END is dropped. */
|
|
780
|
+
function rustStripTrailingClone(expr) {
|
|
781
|
+
return expr.endsWith(".clone()") ? expr.slice(0, -".clone()".length) : expr;
|
|
782
|
+
}
|
|
176
783
|
/** makeRustExprBackend — renders native-expr for Rust. `resolveHead` maps a ref head to an OWNED typed
|
|
177
784
|
* base expr (a prior-node cell clone / an input field / a $as element). Sets rustExprUsed.helpers when
|
|
178
785
|
* a fallible helper is emitted. The runner is Result<_, BehaviorError>, so hoistFallible uses `?`. */
|
|
@@ -216,47 +823,39 @@ function makeRustExprBackend(resolveHead, plan) {
|
|
|
216
823
|
notOp: (a) => `(!${a})`,
|
|
217
824
|
structLit: (name, inits) => `${name} { ${inits.map((i) => `${rustFieldName(i.field)}: ${i.expr}`).join(", ")} }`,
|
|
218
825
|
arrLit: (elemTy, items) => `{ let __v: Vec<${elemTy}> = vec![${items.join(", ")}]; __v }`,
|
|
219
|
-
// a projection port of static string literals —
|
|
220
|
-
//
|
|
826
|
+
// a projection port of static string literals — `vec!["a", "b"]` infers `&'static str` (the field is
|
|
827
|
+
// Vec<&'static str>), ZERO heap allocation.
|
|
221
828
|
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.
|
|
829
|
+
// opt (Option<T>) defaulted to a PURE default — eager unwrap_or for a plain copy/path, else the lazy
|
|
830
|
+
// unwrap_or_else closure (defers an allocating default to the absent branch; clippy or_fun_call).
|
|
237
831
|
optUnwrapOr: (optExpr, defaultExpr, _innerTy) => /^[A-Za-z0-9_.:]+$/.test(defaultExpr) ? `${optExpr}.unwrap_or(${defaultExpr})` : `${optExpr}.unwrap_or_else(|| ${defaultExpr})`,
|
|
238
832
|
// 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.
|
|
833
|
+
// returns must propagate out of the ENCLOSING fn. Only the None arm evaluates the default.
|
|
241
834
|
optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
|
|
242
835
|
`let ${temp}: ${ty} = match ${optExpr} {`,
|
|
243
836
|
` Some(${bindTemp}) => ${bindTemp},`,
|
|
244
837
|
` None => { ${rustBlockBody(dStmts, dExpr)} }`,
|
|
245
838
|
`};`,
|
|
246
839
|
],
|
|
840
|
+
// an opt (`Option<T>`) NONE literal. Annotate as `Option::<T>::None` so it type-infers in ANY
|
|
841
|
+
// position (a bare `None` can be ambiguous; the explicit turbofish keeps it a fully-typed value).
|
|
842
|
+
optNone: (innerTy) => `Option::<${innerTy}>::None`,
|
|
843
|
+
// opt (`Option<T>`) null-presence test: present = `expr.is_some()`, absent = `expr.is_none()`.
|
|
844
|
+
optIsSome: (expr) => `${rustStripTrailingClone(expr)}.is_some()`,
|
|
845
|
+
optIsNone: (expr) => `${rustStripTrailingClone(expr)}.is_none()`,
|
|
247
846
|
ternary: (cond, t, e, _ty) => `(if ${rustStripOuterParens(cond)} { ${rustStripOuterParens(t)} } else { ${rustStripOuterParens(e)} })`,
|
|
248
847
|
shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
|
|
249
848
|
// and: let temp = if left { <rStmts> rExpr } else { false }; or: if left { true } else { <rStmts> rExpr }.
|
|
250
849
|
// build a block that runs rStmts then yields rExpr (parens stripped — block-tail is not a sub-expr).
|
|
251
|
-
const rblock = `{ ${
|
|
850
|
+
const rblock = `{ ${rStmts.join(" ")} ${rustStripOuterParens(rExpr)} }`;
|
|
252
851
|
if (op === "and") {
|
|
253
852
|
return [`let ${temp}: bool = if ${rustStripOuterParens(left)} ${rblock} else { false };`];
|
|
254
853
|
}
|
|
255
854
|
return [`let ${temp}: bool = if ${rustStripOuterParens(left)} { true } else ${rblock};`];
|
|
256
855
|
},
|
|
257
856
|
condGuard: (temp, ty, cond, tStmts, tExpr, eStmts, eExpr) => {
|
|
258
|
-
const tblock = `{ ${
|
|
259
|
-
const eblock = `{ ${
|
|
857
|
+
const tblock = `{ ${tStmts.join(" ")} ${rustStripOuterParens(tExpr)} }`;
|
|
858
|
+
const eblock = `{ ${eStmts.join(" ")} ${rustStripOuterParens(eExpr)} }`;
|
|
260
859
|
return [`let ${temp}: ${ty} = if ${rustStripOuterParens(cond)} ${tblock} else ${eblock};`];
|
|
261
860
|
},
|
|
262
861
|
};
|
|
@@ -265,17 +864,6 @@ function makeRustExprBackend(resolveHead, plan) {
|
|
|
265
864
|
function rustSnake(name) {
|
|
266
865
|
return name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").replace(/([0-9])([a-z])/g, "$1_$2").toLowerCase();
|
|
267
866
|
}
|
|
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
867
|
/** strip ONE redundant outer paren pair from an expr (clippy `if (cond)` unused_parens). Only when the
|
|
280
868
|
* whole string is a single balanced `( … )` — a `(a) && (b)` stays untouched. */
|
|
281
869
|
function rustStripOuterParens(expr) {
|
|
@@ -293,12 +881,6 @@ function rustStripOuterParens(expr) {
|
|
|
293
881
|
}
|
|
294
882
|
return expr.slice(1, -1);
|
|
295
883
|
}
|
|
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
884
|
/** Rust float literal (finite; integral gets `.0`). */
|
|
303
885
|
function rustFloatLitExpr(n) {
|
|
304
886
|
if (Number.isInteger(n))
|
|
@@ -392,13 +974,14 @@ fn bc_expr_str_ge(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) != std::cmp::
|
|
|
392
974
|
// ── CONCRETE per-node row structs + row→outType copiers (mirror the Go concrete contract) ──────────
|
|
393
975
|
//
|
|
394
976
|
// Each covered node has a CONCRETE row struct `RawRowNR<Comp><Node>` whose fields are the node's projected
|
|
395
|
-
// outType fields (typed, flattened)
|
|
977
|
+
// outType fields (typed, flattened). A row models a SUCCESS: failure travels the Result's Err arm as a
|
|
978
|
+
// concrete BehaviorError carrying the leaf's structured Error Value. A named-struct outType
|
|
396
979
|
// flattens to one Rust field per outType field; a scalar/arr/opt node carries a single `val: <typed>`
|
|
397
980
|
// payload. The per-component `HandlerNR<Comp>` trait has one method per node returning that concrete
|
|
398
981
|
// struct; the runner reads `row.<field>` DIRECTLY (no `RawValue`, no `match`, no `Value::`) and copies
|
|
399
982
|
// each field into the node's outType struct local.
|
|
400
983
|
/** emitRawRowStructs — emit the concrete `RawRowNR<Comp><Node>` struct per covered node (+ per-element
|
|
401
|
-
* `RawElemNR<Comp><Node>` for a covered map).
|
|
984
|
+
* `RawElemNR<Comp><Node>` for a covered map). A row models a SUCCESS; failure is the Result's Err arm. */
|
|
402
985
|
function emitRawRowStructs(comp, typedNodes, plan) {
|
|
403
986
|
const parts = [];
|
|
404
987
|
for (const n of comp.body) {
|
|
@@ -409,7 +992,7 @@ function emitRawRowStructs(comp, typedNodes, plan) {
|
|
|
409
992
|
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
410
993
|
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
411
994
|
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
|
|
995
|
+
`#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
|
|
413
996
|
continue;
|
|
414
997
|
}
|
|
415
998
|
if ("map" in n) {
|
|
@@ -417,22 +1000,20 @@ function emitRawRowStructs(comp, typedNodes, plan) {
|
|
|
417
1000
|
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
418
1001
|
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
419
1002
|
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
|
|
1003
|
+
`#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
|
|
421
1004
|
continue;
|
|
422
1005
|
}
|
|
423
1006
|
parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
|
|
424
1007
|
}
|
|
425
1008
|
return parts.join("\n\n");
|
|
426
1009
|
}
|
|
427
|
-
/** Emit one concrete row struct: flatten a named outType to typed fields, else a single `val` payload
|
|
428
|
-
*
|
|
1010
|
+
/** Emit one concrete row struct: flatten a named outType to typed fields, else a single `val` payload.
|
|
1011
|
+
* A row models a SUCCESS — failure is the Result's Err arm, so no in-band error signal. */
|
|
429
1012
|
function emitOneRowStruct(name, ref, plan) {
|
|
430
1013
|
const lines = [];
|
|
431
|
-
lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType
|
|
1014
|
+
lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType).`);
|
|
432
1015
|
lines.push(`#[derive(Default)]`);
|
|
433
1016
|
lines.push(`pub struct ${name} {`);
|
|
434
|
-
lines.push(` pub is_error: bool,`);
|
|
435
|
-
lines.push(` pub err: String,`);
|
|
436
1017
|
if (ref.kind === "named") {
|
|
437
1018
|
const decl = findDecl(plan, ref.name);
|
|
438
1019
|
for (const f of decl.fields) {
|
|
@@ -507,6 +1088,19 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
507
1088
|
lines.push(`// / boxed-value / dynamic accessor crosses the covered boundary — the consumer implements each`);
|
|
508
1089
|
lines.push(`// node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
|
|
509
1090
|
lines.push(`pub trait ${handlerTraitName(comp.name)} {`);
|
|
1091
|
+
// a record-read node returns the consumer's WIRE (an associated `Wire: WireRow`); the generated
|
|
1092
|
+
// decode turns it into the concrete row. Declared once when the component has a de-box-eligible node.
|
|
1093
|
+
// The `+ Send` bound lets the wire cross the parallel-stage runner's scoped fan-out threads (the old
|
|
1094
|
+
// concrete row was Send; the associated type needs the bound stated explicitly).
|
|
1095
|
+
const hasDeBox = comp.body.some((n) => {
|
|
1096
|
+
if ("cond" in n)
|
|
1097
|
+
return false;
|
|
1098
|
+
if ("map" in n || "fanout" in n)
|
|
1099
|
+
return mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
1100
|
+
return deBoxEligible(typedNodes.get(n.id));
|
|
1101
|
+
});
|
|
1102
|
+
if (hasDeBox)
|
|
1103
|
+
lines.push(` type Wire: WireRow + Send;`);
|
|
510
1104
|
for (const n of comp.body) {
|
|
511
1105
|
if ("cond" in n)
|
|
512
1106
|
continue; // #108: a cond node has no handler method (pure Expression).
|
|
@@ -514,21 +1108,29 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
514
1108
|
const fnKw = fnKwFor(n.id);
|
|
515
1109
|
const bt = boundRustType(n, typedNodes, plan);
|
|
516
1110
|
if ("fanout" in n) {
|
|
517
|
-
// fanout (v3): one batched dispatch of the deduped id list
|
|
1111
|
+
// fanout (v3): one batched dispatch of the deduped id list. A record element returns the aligned
|
|
1112
|
+
// element WIRES the runner de-boxes (decode_wire_elem_*) then dedupe/drops. Each aligned position is
|
|
1113
|
+
// Option<Self::Wire> — None = a DANGLING id (the runner maps it to the zero elem row).
|
|
518
1114
|
const portsT = `${portsStructName(comp.name, n.id)}Batch`;
|
|
519
|
-
const
|
|
520
|
-
|
|
1115
|
+
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
1116
|
+
const retT = elemDeBox ? "Vec<Option<Self::Wire>>" : rawRowStructName(comp.name, n.id);
|
|
1117
|
+
lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Result<${retT}, BehaviorError>;`);
|
|
521
1118
|
continue;
|
|
522
1119
|
}
|
|
523
1120
|
if ("map" in n) {
|
|
524
1121
|
const m = n.map;
|
|
525
1122
|
const batched = m.batched === true;
|
|
526
1123
|
const portsT = batched ? `${portsStructName(comp.name, n.id)}Batch` : portsStructName(comp.name, n.id);
|
|
527
|
-
|
|
528
|
-
|
|
1124
|
+
// a record-element map returns the element WIRE(s): batched → Vec<Self::Wire>, nonbatched → Self::Wire.
|
|
1125
|
+
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
1126
|
+
const retT = elemDeBox ? (batched ? "Vec<Self::Wire>" : "Self::Wire") : batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
|
|
1127
|
+
lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Result<${retT}, BehaviorError>;`);
|
|
529
1128
|
continue;
|
|
530
1129
|
}
|
|
531
|
-
|
|
1130
|
+
const ref = typedNodes.get(n.id);
|
|
1131
|
+
const top = ref !== undefined ? recordTop(ref) : null;
|
|
1132
|
+
const retT = top !== null ? rustWireTypeForTop(top) : rawRowStructName(comp.name, n.id);
|
|
1133
|
+
lines.push(` ${fnKw} ${method}(&self, ports: &${portsStructName(comp.name, n.id)}, bound: ${bt}) -> Result<${retT}, BehaviorError>;`);
|
|
532
1134
|
}
|
|
533
1135
|
lines.push(`}`);
|
|
534
1136
|
return lines.join("\n");
|
|
@@ -541,18 +1143,16 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
541
1143
|
function mapElemMaterializerName(compName, nodeId) {
|
|
542
1144
|
return `materialize_map_elem_${sanitize(compName)}_${typedCell(nodeId)}`;
|
|
543
1145
|
}
|
|
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. */
|
|
1146
|
+
/** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType`.
|
|
1147
|
+
*
|
|
1148
|
+
* A map node's `outType` is its ELEMENT type (scp-error.md "Map element type"), so the produced array
|
|
1149
|
+
* is `{arr: element}`. This is the same reading the compiler applies when it resolves what a node
|
|
1150
|
+
* result reference has (`type-gate`: `"map" in n ? {arr:ot} : ot`) — the emitter follows that SSoT
|
|
1151
|
+
* rather than inspecting the annotation's shape, so an element that is ITSELF an `arr` stays
|
|
1152
|
+
* expressible (element `arr(T)` produces `arr(arr(T))`). */
|
|
552
1153
|
function mapNodeArrRef(node, plan) {
|
|
553
1154
|
const outT = node.outType;
|
|
554
|
-
|
|
555
|
-
return deriveTypeRef((isArr ? outT : { arr: outT }), plan);
|
|
1155
|
+
return deriveTypeRef({ arr: outT }, plan);
|
|
556
1156
|
}
|
|
557
1157
|
/** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
|
|
558
1158
|
* type (into) or the element outType directly (no-into). This is the type of RawElemNR<Comp><Node>. */
|
|
@@ -664,26 +1264,42 @@ function reconstructR(e) {
|
|
|
664
1264
|
return e.node;
|
|
665
1265
|
}
|
|
666
1266
|
}
|
|
667
|
-
/**
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1267
|
+
/**
|
|
1268
|
+
* numLiteralRustExpr — a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
|
|
1269
|
+
* `classifyExpr` leaves a bare number `dynamic` (its checked int/float range rules are the evaluate
|
|
1270
|
+
* SSoT), so it never reaches the static str/bool/null branch — but a number literal IS statically
|
|
1271
|
+
* resolvable to a native `Value`: the SAME range check the runtime applies (integral literal in the
|
|
1272
|
+
* safe 2^53-1 window → Value::Int; a fractional/exponent literal → the checked Value::Float) is
|
|
1273
|
+
* performed HERE at generation time. Returns the Rust expression for the literal's Value, or null
|
|
1274
|
+
* when the node is not a bare number OR an integral literal is out of the safe window (keeping the
|
|
1275
|
+
* runtime INVALID_LITERAL semantics — the port is not covered natively, the component falls through).
|
|
1276
|
+
* Mirrors behavior_contracts eval of a bare number literal exactly.
|
|
1277
|
+
*/
|
|
1278
|
+
const NUM_SAFE_INT_R = 9007199254740991; // 2^53 - 1 (expr safeInt)
|
|
1279
|
+
function numLiteralRustExpr(node) {
|
|
1280
|
+
if (typeof node !== "number" || !Number.isFinite(node))
|
|
1281
|
+
return null;
|
|
1282
|
+
if (Number.isInteger(node)) {
|
|
1283
|
+
if (node < -NUM_SAFE_INT_R || node > NUM_SAFE_INT_R)
|
|
1284
|
+
return null; // out of safe window → not covered
|
|
1285
|
+
return `Value::Int(${node}i64)`;
|
|
1286
|
+
}
|
|
1287
|
+
// fractional / exponent literal → checked f64 (finite, guaranteed above).
|
|
1288
|
+
return `Value::Float(${node}f64)`;
|
|
1289
|
+
}
|
|
1290
|
+
/** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
|
|
1291
|
+
* resolvable to a native Value with no Obj / no evaluate interpreter. */
|
|
1292
|
+
/** planForComp — a TypePlan for a single component (its named types are component-scoped). */
|
|
1293
|
+
function planForComp(comp) {
|
|
1294
|
+
return buildTypePlan({ irVersion: 1, exprVersion: 2, components: [comp] });
|
|
1295
|
+
}
|
|
1296
|
+
/** buildTypedNodes — the prior-node typed-cell TypeRef map (control-gate nodes typeless; a map node
|
|
1297
|
+
* carries its produced-array ref). An untyped node is omitted. */
|
|
681
1298
|
function buildTypedNodes(comp, plan) {
|
|
682
1299
|
const typedNodes = new Map();
|
|
683
1300
|
for (const n of comp.body) {
|
|
684
1301
|
if (isControlGate(n))
|
|
685
1302
|
continue;
|
|
686
|
-
// coverage probes under-typed components too; an unresolvable node just isn't a prior-node cell.
|
|
687
1303
|
try {
|
|
688
1304
|
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
|
|
689
1305
|
}
|
|
@@ -693,6 +1309,26 @@ function buildTypedNodes(comp, plan) {
|
|
|
693
1309
|
}
|
|
694
1310
|
return typedNodes;
|
|
695
1311
|
}
|
|
1312
|
+
/** mapAsBinding — the `$as` element binding for a map node's element ports, or undefined (uncovered). */
|
|
1313
|
+
function mapAsBinding(comp, node, typedNodes, plan) {
|
|
1314
|
+
try {
|
|
1315
|
+
return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
|
|
1316
|
+
}
|
|
1317
|
+
catch {
|
|
1318
|
+
return undefined;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
/** portIsStatic — the ELIGIBILITY gate ASKS the one port lowering (compilePortNode / native-expr) whether
|
|
1322
|
+
* it can lower this port. A port that does not lower (or lowers fallibly) fails closed. */
|
|
1323
|
+
function portIsStatic(node, comp, plan, typedNodes, asBinding) {
|
|
1324
|
+
try {
|
|
1325
|
+
compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), "port", asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined);
|
|
1326
|
+
return true;
|
|
1327
|
+
}
|
|
1328
|
+
catch {
|
|
1329
|
+
return false;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
696
1332
|
/** The output must lower to a typed struct/value with no scope read: a ref to a BODY NODE, an
|
|
697
1333
|
* obj/arr of such, or a concat. A ref to an input param / non-concat operator is not lowerable. */
|
|
698
1334
|
function outputIsNativeLowerable(comp) {
|
|
@@ -724,7 +1360,7 @@ function outputIsNativeLowerable(comp) {
|
|
|
724
1360
|
/**
|
|
725
1361
|
* coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet), rust twin of the go predicate.
|
|
726
1362
|
*/
|
|
727
|
-
function coveredMapNode(n, bodyIds, comp
|
|
1363
|
+
function coveredMapNode(n, bodyIds, comp) {
|
|
728
1364
|
if (n === null || typeof n !== "object" || !("map" in n))
|
|
729
1365
|
return false;
|
|
730
1366
|
const m = n.map;
|
|
@@ -756,25 +1392,17 @@ function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
|
|
|
756
1392
|
return false;
|
|
757
1393
|
}
|
|
758
1394
|
const ports = m.ports;
|
|
1395
|
+
const plan = planForComp(comp);
|
|
1396
|
+
const typedNodes = buildTypedNodes(comp, plan);
|
|
759
1397
|
const asBinding = mapAsBinding(comp, n, typedNodes, plan);
|
|
760
1398
|
for (const p of Object.keys(ports))
|
|
761
1399
|
if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
|
|
762
1400
|
return false;
|
|
763
1401
|
return true;
|
|
764
1402
|
}
|
|
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
1403
|
/** coveredFanoutNode (v3, rust twin) — a first-class fanout node is covered when its over is a covered
|
|
776
1404
|
* ref (prior-node arr / input array with elemType) and its element ports are static. See the go twin. */
|
|
777
|
-
function coveredFanoutNode(n, bodyIds, comp
|
|
1405
|
+
function coveredFanoutNode(n, bodyIds, comp) {
|
|
778
1406
|
if (n === null || typeof n !== "object" || !("fanout" in n))
|
|
779
1407
|
return false;
|
|
780
1408
|
const f = n.fanout;
|
|
@@ -794,6 +1422,8 @@ function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
|
|
|
794
1422
|
return false;
|
|
795
1423
|
}
|
|
796
1424
|
const ports = f.ports;
|
|
1425
|
+
const plan = planForComp(comp);
|
|
1426
|
+
const typedNodes = buildTypedNodes(comp, plan);
|
|
797
1427
|
let asBinding;
|
|
798
1428
|
try {
|
|
799
1429
|
asBinding = { name: f.as, ref: fanoutOverElemInfo(comp, n, typedNodes, plan).elemRef, plan };
|
|
@@ -838,8 +1468,8 @@ function fanoutOverElemInfo(comp, node, typedNodes, plan) {
|
|
|
838
1468
|
}
|
|
839
1469
|
/** A covered read SHAPE with static ports + native-lowerable output (+ covered map...into), OR a
|
|
840
1470
|
* real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point reads. */
|
|
841
|
-
function isSequentialComponentRefRead(comp
|
|
842
|
-
return coverageRejectReason(comp
|
|
1471
|
+
function isSequentialComponentRefRead(comp) {
|
|
1472
|
+
return coverageRejectReason(comp) === null;
|
|
843
1473
|
}
|
|
844
1474
|
/**
|
|
845
1475
|
* coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89), rust twin of
|
|
@@ -848,11 +1478,12 @@ function isSequentialComponentRefRead(comp, plan) {
|
|
|
848
1478
|
* IDENTICAL to isSequentialComponentRefRead (defined as `coverageRejectReason(c) === null`) so the two
|
|
849
1479
|
* can never disagree. This is the message the fail-closed dispatch surfaces per non-native component.
|
|
850
1480
|
*/
|
|
851
|
-
function coverageRejectReason(comp
|
|
1481
|
+
function coverageRejectReason(comp) {
|
|
1482
|
+
const crPlan = planForComp(comp);
|
|
1483
|
+
const crTypedNodes = buildTypedNodes(comp, crPlan);
|
|
852
1484
|
const ep = execPlan(comp);
|
|
853
1485
|
if (ep === null)
|
|
854
1486
|
return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
|
|
855
|
-
const typedNodes = buildTypedNodes(comp, plan);
|
|
856
1487
|
// #114: a bindField relation child (single|connection) IS covered inside a parallel stage — the
|
|
857
1488
|
// sibling hasMany fan-out (parent → members ∥ permissions). Its bound-key skip-gate is preflighted
|
|
858
1489
|
// per member (from the settled parent) BEFORE dispatch, so the scoped-thread fan-out keeps
|
|
@@ -890,12 +1521,12 @@ function coverageRejectReason(comp, plan) {
|
|
|
890
1521
|
continue;
|
|
891
1522
|
}
|
|
892
1523
|
if ("fanout" in n) {
|
|
893
|
-
if (!coveredFanoutNode(n, bodyIds, comp
|
|
1524
|
+
if (!coveredFanoutNode(n, bodyIds, comp))
|
|
894
1525
|
return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
|
|
895
1526
|
continue;
|
|
896
1527
|
}
|
|
897
1528
|
if ("map" in n) {
|
|
898
|
-
if (!coveredMapNode(n, bodyIds, comp
|
|
1529
|
+
if (!coveredMapNode(n, bodyIds, comp))
|
|
899
1530
|
return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
|
|
900
1531
|
continue;
|
|
901
1532
|
}
|
|
@@ -914,7 +1545,7 @@ function coverageRejectReason(comp, plan) {
|
|
|
914
1545
|
if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
915
1546
|
return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
|
|
916
1547
|
for (const p of Object.keys(cr.ports))
|
|
917
|
-
if (portIsStatic(cr.ports[p], comp,
|
|
1548
|
+
if (portIsStatic(cr.ports[p], comp, crPlan, crTypedNodes) === false)
|
|
918
1549
|
return `node '${n.id}' port '${p}' is not statically resolvable`;
|
|
919
1550
|
}
|
|
920
1551
|
if (!outputIsNativeLowerable(comp))
|
|
@@ -931,42 +1562,28 @@ function isFullyTyped(comp) {
|
|
|
931
1562
|
return false;
|
|
932
1563
|
return true;
|
|
933
1564
|
}
|
|
934
|
-
function isNativeRaw(comp
|
|
935
|
-
return isSequentialComponentRefRead(comp
|
|
1565
|
+
function isNativeRaw(comp) {
|
|
1566
|
+
return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
|
|
936
1567
|
}
|
|
937
|
-
function assertTypedCoverage(comp
|
|
938
|
-
if (!isSequentialComponentRefRead(comp
|
|
1568
|
+
function assertTypedCoverage(comp) {
|
|
1569
|
+
if (!isSequentialComponentRefRead(comp))
|
|
939
1570
|
return;
|
|
940
1571
|
if (isFullyTyped(comp))
|
|
941
1572
|
return;
|
|
942
1573
|
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
1574
|
}
|
|
944
1575
|
// ── 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
|
-
*/
|
|
1576
|
+
/** Rust type for an input port's declared type (scalar → concrete; array+elemType → Vec<ElemT>; else Value). */
|
|
955
1577
|
function inputPortRustType(schema, plan, where = "input port") {
|
|
956
1578
|
const ref = inputPortTypeRef(schema, plan);
|
|
957
1579
|
if (ref !== undefined)
|
|
958
1580
|
return renderTypeRef(ref);
|
|
959
1581
|
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
|
|
1582
|
+
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
1583
|
return "Value";
|
|
962
1584
|
}
|
|
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`. */
|
|
1585
|
+
/** The scalar kind an input port declares, or undefined if it is not a native scalar. */
|
|
967
1586
|
function inputScalarKind(schema) {
|
|
968
|
-
if (inputPortIsOptional(schema))
|
|
969
|
-
return undefined;
|
|
970
1587
|
switch (schema?.type) {
|
|
971
1588
|
case "string":
|
|
972
1589
|
case "literal": // a `"literal"` union is a constrained String (see inputPortRustType).
|
|
@@ -990,6 +1607,8 @@ function emitInStruct(comp, plan) {
|
|
|
990
1607
|
if (ports.length === 0) {
|
|
991
1608
|
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\n#[derive(Default)]\npub struct ${name};`;
|
|
992
1609
|
}
|
|
1610
|
+
// #139: inputPortRustType reads the inputPortTypeRef SSoT — an OPTIONAL port is `Option<T>` (absent →
|
|
1611
|
+
// None); an optional whose inner type has no native lowering FAILS CLOSED (never a boxed escape).
|
|
993
1612
|
const fields = ports
|
|
994
1613
|
.map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`)}, // ${JSON.stringify(p)}`)
|
|
995
1614
|
.join("\n");
|
|
@@ -1000,18 +1619,30 @@ pub struct ${name} {
|
|
|
1000
1619
|
${fields}
|
|
1001
1620
|
}`;
|
|
1002
1621
|
}
|
|
1003
|
-
/**
|
|
1004
|
-
*
|
|
1622
|
+
/** A ref resolver for native lowering: given a ref head + remaining field path, return a native scalar
|
|
1623
|
+
* Rust expression (typed input field / prior-node typed struct scalar / element binding field), or null
|
|
1624
|
+
* when the ref does not resolve to a REQUIRED (non-opt) native scalar (then the caller falls back to the
|
|
1625
|
+
* Value path). Returned exprs are OWNED (String cloned / scalar copied) — usable in a `format!` / literal. */
|
|
1626
|
+
// bc#90 / runtime-free: the old `emitStaticR` / `inputLeafR` / `valueLeafR` port lowering (which built
|
|
1627
|
+
// `Value` leaves via the primitives::* helpers and struct-field serializers) is REMOVED — every covered
|
|
1628
|
+
// port now lowers to a FULLY NATIVE Rust value in emitPortInit (string / bool / static-string-array /
|
|
1629
|
+
// number literal), and a genuinely-dynamic port fails closed. There is no boxed-Value fallback on the
|
|
1630
|
+
// covered read plane.
|
|
1005
1631
|
/**
|
|
1006
|
-
* emitPortInit — the port field
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1632
|
+
* emitPortInit — emit the port field build for one port as a FULLY NATIVE Rust expression (bc#90 /
|
|
1633
|
+
* runtime-free). Every covered port lowers to a concrete Rust value assigned straight into the ports
|
|
1634
|
+
* struct field — NO `Value`, NO fallback path, NO `match`:
|
|
1635
|
+
* - a static string array (projection) → `vec!["a", "b", ...]` (change #2);
|
|
1636
|
+
* - a bare int/float number literal (e.g. Query `limit: 20`) → `20i64` / `<n>f64`;
|
|
1637
|
+
* - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
|
|
1638
|
+
* A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered plane —
|
|
1639
|
+
* a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
|
|
1640
|
+
* ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
|
|
1010
1641
|
*/
|
|
1011
1642
|
function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
|
|
1012
1643
|
const c = compilePortNode(expr, comp, plan, typedNodes, isPrior, `port '${pn}'`, opts);
|
|
1013
1644
|
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 (
|
|
1645
|
+
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
1646
|
return `${portFieldName(pn)}: ${c.expr}`;
|
|
1016
1647
|
}
|
|
1017
1648
|
// ── the combined runner (STRUCT-returning; INLINE sequential; native ports in; concrete row out) ──
|
|
@@ -1216,22 +1847,57 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1216
1847
|
// directly (no `.await`). The runner being `async fn` does not force every call site to await.
|
|
1217
1848
|
const awaitSfx = ap.nodeIsAsync(node.id) ? ".await" : "";
|
|
1218
1849
|
lines.push(`${indent}let ports_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1850
|
+
// a de-box-eligible node returns the wire (Self::Wire / Vec / Option / BTreeMap); the generated
|
|
1851
|
+
// decode_wire_result_* probes each record strictly and produces the structured error on a mismatch
|
|
1852
|
+
// (byte-equal codes to run_behavior's outType check, so ≡ holds). A non-de-box node keeps its direct
|
|
1853
|
+
// concrete-row move.
|
|
1854
|
+
const top = recordTop(ref);
|
|
1855
|
+
const namedTop = top !== null && top.kind === "named";
|
|
1856
|
+
const deBox = top !== null;
|
|
1223
1857
|
if (meta.policy === "continue") {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1858
|
+
// continue: a failed node (leaf failure OR de-box mismatch) produces no Port — the downstream
|
|
1859
|
+
// Skip propagation is what `continue` means. Only a clean decode commits the result.
|
|
1860
|
+
if (namedTop) {
|
|
1861
|
+
lines.push(`${indent}if let Ok(wire_${s}) = handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
|
|
1862
|
+
lines.push(`${indent} if let Ok(row_${s}) = ${rustDecodeNamedName(top.innerName)}(&wire_${s}) {`);
|
|
1863
|
+
lines.push(`${indent} ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
1864
|
+
lines.push(`${indent} produced_${s}.set(true);`);
|
|
1865
|
+
lines.push(`${indent} }`);
|
|
1866
|
+
lines.push(`${indent}}`);
|
|
1867
|
+
}
|
|
1868
|
+
else if (deBox) {
|
|
1869
|
+
lines.push(`${indent}if let Ok(wire_${s}) = handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
|
|
1870
|
+
lines.push(`${indent} if let Ok(res_${s}) = ${rustDecodeResultName(comp.name, node.id)}(wire_${s}) {`);
|
|
1871
|
+
lines.push(`${indent} *${typedCell(node.id)}.borrow_mut() = res_${s};`);
|
|
1872
|
+
lines.push(`${indent} produced_${s}.set(true);`);
|
|
1873
|
+
lines.push(`${indent} }`);
|
|
1874
|
+
lines.push(`${indent}}`);
|
|
1875
|
+
}
|
|
1876
|
+
else {
|
|
1877
|
+
lines.push(`${indent}if let Ok(row_${s}) = handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
|
|
1878
|
+
lines.push(`${indent} ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
1879
|
+
lines.push(`${indent} produced_${s}.set(true);`);
|
|
1880
|
+
lines.push(`${indent}}`);
|
|
1881
|
+
}
|
|
1228
1882
|
}
|
|
1229
1883
|
else {
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1232
|
-
lines.push(`${indent}
|
|
1233
|
-
lines.push(`${indent}
|
|
1234
|
-
lines.push(`${indent}
|
|
1884
|
+
const policyLit = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
|
|
1885
|
+
const resultVar = deBox ? `wire_${s}` : `row_${s}`;
|
|
1886
|
+
lines.push(`${indent}let ${resultVar} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
|
|
1887
|
+
lines.push(`${indent} Ok(r) => r,`);
|
|
1888
|
+
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 }),`);
|
|
1889
|
+
lines.push(`${indent}};`);
|
|
1890
|
+
if (namedTop) {
|
|
1891
|
+
lines.push(`${indent}let row_${s} = ${rustDecodeNamedName(top.innerName)}(&wire_${s})?;`);
|
|
1892
|
+
lines.push(`${indent}${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
1893
|
+
}
|
|
1894
|
+
else if (deBox) {
|
|
1895
|
+
lines.push(`${indent}let res_${s} = ${rustDecodeResultName(comp.name, node.id)}(wire_${s})?;`);
|
|
1896
|
+
lines.push(`${indent}*${typedCell(node.id)}.borrow_mut() = res_${s};`);
|
|
1897
|
+
}
|
|
1898
|
+
else {
|
|
1899
|
+
lines.push(`${indent}${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
1900
|
+
}
|
|
1235
1901
|
lines.push(`${indent}produced_${s}.set(true);`);
|
|
1236
1902
|
}
|
|
1237
1903
|
for (const c of closers)
|
|
@@ -1356,10 +2022,26 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1356
2022
|
const nMembers = refMembers.length;
|
|
1357
2023
|
const boundN = Math.min(concurrency, nMembers);
|
|
1358
2024
|
const jobsVar = `jobs_${sanitize(comp.name)}`;
|
|
2025
|
+
const hWire = `<H as ${handlerTraitName(comp.name)}>::Wire`;
|
|
2026
|
+
const hWireTop = (top) => {
|
|
2027
|
+
switch (top.kind) {
|
|
2028
|
+
case "named":
|
|
2029
|
+
return hWire;
|
|
2030
|
+
case "arr":
|
|
2031
|
+
return `Vec<${hWire}>`;
|
|
2032
|
+
case "opt":
|
|
2033
|
+
return `Option<${hWire}>`;
|
|
2034
|
+
case "map":
|
|
2035
|
+
return `std::collections::BTreeMap<String, ${hWire}>`;
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
1359
2038
|
for (const i of refMembers) {
|
|
1360
2039
|
const s = sanitize(comp.body[i].id);
|
|
1361
|
-
|
|
1362
|
-
|
|
2040
|
+
// a de-box member's thread returns the WIRE (shaped by the record top); a non-de-box member returns
|
|
2041
|
+
// its concrete row directly.
|
|
2042
|
+
const memTop = recordTop(typedNodes.get(comp.body[i].id));
|
|
2043
|
+
const slotT = memTop !== null ? hWireTop(memTop) : rawRowStructName(comp.name, comp.body[i].id);
|
|
2044
|
+
lines.push(` let slot_${s}: std::sync::Mutex<Option<Result<${slotT}, BehaviorError>>> = std::sync::Mutex::new(None);`);
|
|
1363
2045
|
}
|
|
1364
2046
|
lines.push(` let mut ${jobsVar}: Vec<usize> = Vec::new();`);
|
|
1365
2047
|
refMembers.forEach((i, slot) => {
|
|
@@ -1397,22 +2079,52 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1397
2079
|
const s = sanitize(node.id);
|
|
1398
2080
|
const ref = typedNodes.get(node.id);
|
|
1399
2081
|
lines.push(` if ports_${s}.is_some() {`);
|
|
1400
|
-
lines.push(` let
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
2082
|
+
lines.push(` let out_${s} = slot_${s}.lock().unwrap().take().expect("runnable member wrote its slot");`);
|
|
2083
|
+
const top = recordTop(ref);
|
|
2084
|
+
const namedTop = top !== null && top.kind === "named";
|
|
2085
|
+
const deBox = top !== null;
|
|
1404
2086
|
if (meta.policy === "continue") {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
2087
|
+
if (namedTop) {
|
|
2088
|
+
lines.push(` if let Ok(wire_${s}) = out_${s} {`);
|
|
2089
|
+
lines.push(` if let Ok(row_${s}) = ${rustDecodeNamedName(top.innerName)}(&wire_${s}) {`);
|
|
2090
|
+
lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
2091
|
+
lines.push(` produced_${s}.set(true);`);
|
|
2092
|
+
lines.push(` }`);
|
|
2093
|
+
lines.push(` }`);
|
|
2094
|
+
}
|
|
2095
|
+
else if (deBox) {
|
|
2096
|
+
lines.push(` if let Ok(wire_${s}) = out_${s} {`);
|
|
2097
|
+
lines.push(` if let Ok(res_${s}) = ${rustDecodeResultName(comp.name, node.id)}(wire_${s}) {`);
|
|
2098
|
+
lines.push(` *${typedCell(node.id)}.borrow_mut() = res_${s};`);
|
|
2099
|
+
lines.push(` produced_${s}.set(true);`);
|
|
2100
|
+
lines.push(` }`);
|
|
2101
|
+
lines.push(` }`);
|
|
2102
|
+
}
|
|
2103
|
+
else {
|
|
2104
|
+
lines.push(` if let Ok(row_${s}) = out_${s} {`);
|
|
2105
|
+
lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
2106
|
+
lines.push(` produced_${s}.set(true);`);
|
|
2107
|
+
lines.push(` }`);
|
|
2108
|
+
}
|
|
1409
2109
|
}
|
|
1410
2110
|
else {
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
lines.push(`
|
|
1414
|
-
lines.push(`
|
|
1415
|
-
lines.push(`
|
|
2111
|
+
const policyLit = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
|
|
2112
|
+
const resultVar = deBox ? `wire_${s}` : `row_${s}`;
|
|
2113
|
+
lines.push(` let ${resultVar} = match out_${s} {`);
|
|
2114
|
+
lines.push(` Ok(r) => r,`);
|
|
2115
|
+
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 }),`);
|
|
2116
|
+
lines.push(` };`);
|
|
2117
|
+
if (namedTop) {
|
|
2118
|
+
lines.push(` let row_${s} = ${rustDecodeNamedName(top.innerName)}(&wire_${s})?;`);
|
|
2119
|
+
lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
2120
|
+
}
|
|
2121
|
+
else if (deBox) {
|
|
2122
|
+
lines.push(` let res_${s} = ${rustDecodeResultName(comp.name, node.id)}(wire_${s})?;`);
|
|
2123
|
+
lines.push(` *${typedCell(node.id)}.borrow_mut() = res_${s};`);
|
|
2124
|
+
}
|
|
2125
|
+
else {
|
|
2126
|
+
lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
|
|
2127
|
+
}
|
|
1416
2128
|
lines.push(` produced_${s}.set(true);`);
|
|
1417
2129
|
}
|
|
1418
2130
|
lines.push(` }`);
|
|
@@ -1449,18 +2161,28 @@ isAsync = false) {
|
|
|
1449
2161
|
const portNames = Object.keys(m.ports);
|
|
1450
2162
|
const elemArrRef = typedNodes.get(node.id);
|
|
1451
2163
|
const elemName = renderTypeRef(elemArrRef.elem);
|
|
1452
|
-
|
|
2164
|
+
// a record element is de-boxed strictly: the handler returns the element WIRE(s) and the arm decodes via
|
|
2165
|
+
// decode_wire_elem_* (closing the scan-row hole). A de-box mismatch fails closed regardless of
|
|
2166
|
+
// elementPolicy (matching the interpreter's whole-node outType throw).
|
|
2167
|
+
const elemDeBox = mapFanoutElemDeBoxEligible(node, typedNodes, plan);
|
|
2168
|
+
const decElemFn = elemDeBox ? rustDecodeElemName(comp.name, node.id) : "";
|
|
1453
2169
|
// build the per-element CONCRETE native ports struct. `il` = indent inside the element loop.
|
|
1454
2170
|
const buildPorts = (il) => {
|
|
1455
|
-
const
|
|
1456
|
-
|
|
2171
|
+
const out = [];
|
|
2172
|
+
const inits = [];
|
|
2173
|
+
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
2174
|
+
for (const pn of portNames) {
|
|
2175
|
+
inits.push(emitPortInit(pn, m.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
|
|
2176
|
+
}
|
|
2177
|
+
out.push(`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
2178
|
+
return out;
|
|
1457
2179
|
};
|
|
1458
2180
|
// #108 guard: compile `when` to a native bool over an element-scoped resolveHead ($as element value,
|
|
1459
2181
|
// prior node, input port). On skip: `continue` (element excluded — matches run_behavior's keep-gate).
|
|
1460
2182
|
const emitGuard = (il) => {
|
|
1461
2183
|
if (!hasGuard)
|
|
1462
2184
|
return [];
|
|
1463
|
-
const resolveHead = nativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` });
|
|
2185
|
+
const resolveHead = nativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding: { name: asName, ref: overElemRef, plan }, asBase: `oel_${s}` });
|
|
1464
2186
|
const g = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compileBool(m.when);
|
|
1465
2187
|
const out = [];
|
|
1466
2188
|
for (const st of g.stmts)
|
|
@@ -1500,26 +2222,30 @@ isAsync = false) {
|
|
|
1500
2222
|
lines.push(`${indent} let want_${s} = items_${s}.len();`);
|
|
1501
2223
|
lines.push(`${indent} let bp_${s} = ${batch} { items: items_${s} };`);
|
|
1502
2224
|
lines.push(`${indent} let row_${s} = match handlers.${nodeMethodName(node.id)}(&bp_${s}, None)${awaitSfx} {`);
|
|
1503
|
-
lines.push(`${indent}
|
|
1504
|
-
lines.push(`${indent}
|
|
2225
|
+
lines.push(`${indent} Ok(r) => r,`);
|
|
2226
|
+
lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
|
|
1505
2227
|
lines.push(`${indent} };`);
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
lines.push(`${indent} }`);
|
|
1509
|
-
lines.push(`${indent} if row_${s}.rows.len() != want_${s} {`);
|
|
2228
|
+
const batchLen = elemDeBox ? `row_${s}.len()` : `row_${s}.rows.len()`;
|
|
2229
|
+
const batchIter = elemDeBox ? `row_${s}.into_iter()` : `row_${s}.rows.into_iter()`;
|
|
2230
|
+
lines.push(`${indent} if ${batchLen} != want_${s} {`);
|
|
1510
2231
|
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
2232
|
lines.push(`${indent} }`);
|
|
1512
2233
|
if (hasInto) {
|
|
1513
|
-
// zip the aligned batch rows onto each KEPT over element (into materializer).
|
|
1514
|
-
|
|
2234
|
+
// zip the aligned batch rows onto each KEPT over element (into materializer). A record element's
|
|
2235
|
+
// wire is de-boxed strictly first.
|
|
2236
|
+
lines.push(`${indent} let mut rows_iter_${s} = ${batchIter};`);
|
|
1515
2237
|
lines.push(`${indent} for over_el_${s} in kept_${s}.iter() {`);
|
|
1516
2238
|
lines.push(`${indent} let er_${s} = rows_iter_${s}.next().expect("aligned batch row");`);
|
|
2239
|
+
if (elemDeBox)
|
|
2240
|
+
lines.push(`${indent} let er_${s} = ${decElemFn}(&er_${s})?;`);
|
|
1517
2241
|
lines.push(`${indent} built_${s}.push(${elemMat}(over_el_${s}, er_${s}));`);
|
|
1518
2242
|
lines.push(`${indent} }`);
|
|
1519
2243
|
}
|
|
1520
2244
|
else {
|
|
1521
2245
|
// no-into: collect each element DIRECTLY from the aligned per-element row (result = kept rows).
|
|
1522
|
-
lines.push(`${indent} for er_${s} in
|
|
2246
|
+
lines.push(`${indent} for er_${s} in ${batchIter} {`);
|
|
2247
|
+
if (elemDeBox)
|
|
2248
|
+
lines.push(`${indent} let er_${s} = ${decElemFn}(&er_${s})?;`);
|
|
1523
2249
|
lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
|
|
1524
2250
|
lines.push(`${indent} built_${s}.push(el_${s});`);
|
|
1525
2251
|
lines.push(`${indent} }`);
|
|
@@ -1533,13 +2259,21 @@ isAsync = false) {
|
|
|
1533
2259
|
lines.push(l);
|
|
1534
2260
|
for (const l of buildPorts(indent + " "))
|
|
1535
2261
|
lines.push(l);
|
|
2262
|
+
// Element Error Policy (scp-error.md): `skip` drops the failing element and proceeds (order is
|
|
2263
|
+
// preserved; the leaf keeps the Error Value it produced). `error` (default) promotes the element
|
|
2264
|
+
// Failure to the map's Component Failure, carrying the leaf's detail through.
|
|
1536
2265
|
lines.push(`${indent} let er_${s} = match handlers.${nodeMethodName(node.id)}(&ep_${s}, None)${awaitSfx} {`);
|
|
1537
|
-
lines.push(`${indent}
|
|
1538
|
-
|
|
2266
|
+
lines.push(`${indent} Ok(r) => r,`);
|
|
2267
|
+
if (m.elementPolicy === "skip") {
|
|
2268
|
+
lines.push(`${indent} Err(_) => continue,`);
|
|
2269
|
+
}
|
|
2270
|
+
else {
|
|
2271
|
+
lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
|
|
2272
|
+
}
|
|
1539
2273
|
lines.push(`${indent} };`);
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
2274
|
+
// a record element's wire is de-boxed strictly (fail-closed regardless of elementPolicy).
|
|
2275
|
+
if (elemDeBox)
|
|
2276
|
+
lines.push(`${indent} let er_${s} = ${decElemFn}(&er_${s})?;`);
|
|
1543
2277
|
if (hasInto) {
|
|
1544
2278
|
lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s}, er_${s}));`);
|
|
1545
2279
|
}
|
|
@@ -1583,9 +2317,12 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
|
|
|
1583
2317
|
if (!dkField || dkField.type.kind !== "scalar" || dkField.type.scalar !== "string")
|
|
1584
2318
|
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
2319
|
const dkRust = rustFieldName(f.dedupeKey);
|
|
1586
|
-
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1587
2320
|
const buildPorts = (il) => {
|
|
1588
|
-
const inits =
|
|
2321
|
+
const inits = [];
|
|
2322
|
+
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
2323
|
+
for (const pn of portNames) {
|
|
2324
|
+
inits.push(emitPortInit(pn, f.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
|
|
2325
|
+
}
|
|
1589
2326
|
return [`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`];
|
|
1590
2327
|
};
|
|
1591
2328
|
const lines = [];
|
|
@@ -1609,20 +2346,26 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
|
|
|
1609
2346
|
lines.push(`${indent} let want_${s} = pi_${s}.len();`);
|
|
1610
2347
|
lines.push(`${indent} let bp_${s} = ${batch} { items: pi_${s} };`);
|
|
1611
2348
|
lines.push(`${indent} let row_${s} = match handlers.${nodeMethodName(node.id)}(&bp_${s}, None)${awaitSfx} {`);
|
|
1612
|
-
lines.push(`${indent}
|
|
1613
|
-
lines.push(`${indent}
|
|
2349
|
+
lines.push(`${indent} Ok(r) => r,`);
|
|
2350
|
+
lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
|
|
1614
2351
|
lines.push(`${indent} };`);
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
2352
|
+
// the fanout element row is a named record → the handler returns the aligned element WIRES
|
|
2353
|
+
// (Vec<Option<Self::Wire>>) and each present body is de-boxed strictly (a de-box mismatch fails closed).
|
|
2354
|
+
// A DANGLING id is None → the zero element row (empty dedupeKey), which the dangling drop excludes.
|
|
2355
|
+
const decElemFn = rustDecodeElemName(comp.name, node.id);
|
|
2356
|
+
const rawElemT = rawElemStructName(comp.name, node.id);
|
|
2357
|
+
lines.push(`${indent} if row_${s}.len() != want_${s} {`);
|
|
1619
2358
|
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
2359
|
lines.push(`${indent} }`);
|
|
1621
2360
|
// ── THE ONE dedupe/drop (mirrors fanoutDedupDrop verbatim): first-seen dedupe by dedupeKey field;
|
|
1622
2361
|
// drop dangling (empty dedupeKey ≡ interpreter's null/absent-key body); implicitSource strip is
|
|
1623
2362
|
// structural (the elem type already omits it). cursor is always None (connection wrap).
|
|
1624
2363
|
lines.push(`${indent} let mut seen_${s}: std::collections::HashSet<String> = std::collections::HashSet::new();`);
|
|
1625
|
-
lines.push(`${indent} for
|
|
2364
|
+
lines.push(`${indent} for opt_er_${s} in row_${s}.into_iter() {`);
|
|
2365
|
+
lines.push(`${indent} let er_${s}: ${rawElemT} = match opt_er_${s} {`);
|
|
2366
|
+
lines.push(`${indent} Some(wire_${s}) => ${decElemFn}(&wire_${s})?,`);
|
|
2367
|
+
lines.push(`${indent} None => Default::default(),`);
|
|
2368
|
+
lines.push(`${indent} };`);
|
|
1626
2369
|
lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemRef, plan)}`);
|
|
1627
2370
|
lines.push(`${indent} let key_${s} = el_${s}.${dkRust}.clone();`);
|
|
1628
2371
|
if (f.drop === "dangling") {
|
|
@@ -1639,73 +2382,6 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
|
|
|
1639
2382
|
lines.push(c);
|
|
1640
2383
|
return lines.join("\n");
|
|
1641
2384
|
}
|
|
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
2385
|
/**
|
|
1710
2386
|
* emitCondArm — the struct-native exec of a covered cond node (#108, rust). Mirrors run_behavior's cond
|
|
1711
2387
|
* lower: compile `if` to a native bool, materialize the TAKEN branch to the node's outType (only the
|
|
@@ -1847,6 +2523,13 @@ pub const COMPONENT_NAMES_NATIVE_RAW: [&str; ${names.length}] = [${names.join(",
|
|
|
1847
2523
|
const UNKNOWN_COMPONENT_HELPER = `#[allow(dead_code)]
|
|
1848
2524
|
fn unknown_component(component: &str) -> BehaviorError {
|
|
1849
2525
|
BehaviorError::new("UNKNOWN_COMPONENT", format!("component '{component}' has no handler (fail-closed)"))
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// leaf_failure — a failure the LEAF reports (test glue): the runner assigns the node's failure code
|
|
2529
|
+
// from its Error Policy Kind, so what a leaf carries is its message and its structured Error Value.
|
|
2530
|
+
#[allow(dead_code)]
|
|
2531
|
+
fn leaf_failure(message: impl Into<String>) -> BehaviorError {
|
|
2532
|
+
BehaviorError::new("LEAF_FAILURE", message)
|
|
1850
2533
|
}`;
|
|
1851
2534
|
// ── module assembly ──────────────────────────────────────────────────────────────────
|
|
1852
2535
|
function emit(ctx) {
|
|
@@ -1855,20 +2538,17 @@ function emit(ctx) {
|
|
|
1855
2538
|
// rust CONSUMES this annotation (async fn + .await where derived); go ignores it. per-node granularity
|
|
1856
2539
|
// is a strict generalization of increment-1's uniform ioModel:'async' (= every terminal async).
|
|
1857
2540
|
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
2541
|
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
|
|
2542
|
+
assertTypedCoverage(c);
|
|
2543
|
+
const native = ctx.ir.components.filter(isNativeRaw);
|
|
2544
|
+
const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
|
|
1865
2545
|
// FAIL CLOSED (bc#86/#89): the --typed-native (native codegen) surface is native-only. Previously a
|
|
1866
2546
|
// non-covered component was SILENTLY DROPPED (emitted only as a comment note), concealing that it
|
|
1867
2547
|
// never got native codegen. We now throw a LOUD GeneratorFailure naming every uncovered component +
|
|
1868
2548
|
// its specific reject reason. A fully-covered module is unchanged.
|
|
1869
2549
|
if (nonNative.length > 0) {
|
|
1870
2550
|
const rejects = nonNative
|
|
1871
|
-
.map((c) => ` - component '${c.name}': ${coverageRejectReason(c
|
|
2551
|
+
.map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
|
|
1872
2552
|
.join("\n");
|
|
1873
2553
|
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
2554
|
`The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
|
|
@@ -1882,6 +2562,7 @@ function emit(ctx) {
|
|
|
1882
2562
|
${emitStructSurface([])}
|
|
1883
2563
|
`;
|
|
1884
2564
|
}
|
|
2565
|
+
const plan = buildTypePlan(ctx.ir);
|
|
1885
2566
|
// The typed-native module is a de-boxed CONSUMER artifact: the covered runner RETURNS these outType
|
|
1886
2567
|
// structs and the consumer reads their fields natively. So the decls (and their FIELDS) are pub — the
|
|
1887
2568
|
// consumer keeps the model native (mirrors Go's exported outType struct fields on the covered path).
|
|
@@ -1898,8 +2579,15 @@ ${emitStructSurface([])}
|
|
|
1898
2579
|
const handlerTraits = [];
|
|
1899
2580
|
const inStructs = [];
|
|
1900
2581
|
const elemMaterializers = [];
|
|
2582
|
+
// the generated strict de-box functions (deduped by named type across all covered components).
|
|
2583
|
+
const decodeFns = [];
|
|
2584
|
+
const decodeEmitted = new Set();
|
|
1901
2585
|
for (const c of native) {
|
|
1902
|
-
const typedNodes =
|
|
2586
|
+
const typedNodes = new Map();
|
|
2587
|
+
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
2588
|
+
for (const n of c.body)
|
|
2589
|
+
if (!isControlGate(n))
|
|
2590
|
+
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan)); // #125: control-gate typeless; map は produced-array ref に正規化
|
|
1903
2591
|
for (const n of c.body) {
|
|
1904
2592
|
if ("fanout" in n)
|
|
1905
2593
|
structs.push(emitFanoutPortsStructs(c, n, typedNodes, plan));
|
|
@@ -1909,10 +2597,13 @@ ${emitStructSurface([])}
|
|
|
1909
2597
|
// #108: a cond node has NO ports struct (pure Expression — no handler dispatch).
|
|
1910
2598
|
}
|
|
1911
2599
|
else
|
|
1912
|
-
structs.push(emitPortsStruct(c, n, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
|
|
2600
|
+
structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
|
|
1913
2601
|
}
|
|
2602
|
+
// fail closed for a record inside a non-named top (never a silent trust) — before emitting anything.
|
|
2603
|
+
assertDeBoxableResults(c, typedNodes, plan);
|
|
1914
2604
|
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
1915
2605
|
handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
|
|
2606
|
+
emitRustNodeDecodeFns(c, typedNodes, plan, decodeEmitted, decodeFns);
|
|
1916
2607
|
inStructs.push(emitInStruct(c, plan));
|
|
1917
2608
|
const mm = emitMapElemMaterializers(c, typedNodes, plan);
|
|
1918
2609
|
if (mm)
|
|
@@ -1955,6 +2646,12 @@ use std::cell::RefCell;`;
|
|
|
1955
2646
|
: undefined,
|
|
1956
2647
|
`// CONCRETE per-component input structs (fields = inputPorts).\n${inStructs.join("\n\n")}`,
|
|
1957
2648
|
`// CONCRETE per-component handler traits (one node_* method per node — native ports IN, row OUT).\n${handlerTraits.join("\n\n")}`,
|
|
2649
|
+
decodeFns.length
|
|
2650
|
+
? `// strict de-box wire seam — the consumer implements WireRow/WireList over its wire payload (variant\n// classification); the generated decode_wire_* owns strictness (required/optional, present/absent, error\n// assembly, fail-closed). Concrete enums + monomorphized traits — no boxed value, no trait object, no\n// heap allocation crosses the seam.\n${emitRustProbeWireTypes()}`
|
|
2651
|
+
: undefined,
|
|
2652
|
+
decodeFns.length
|
|
2653
|
+
? `// Generated strict de-box functions (per named record type; structured Error Value on mismatch).\n${decodeFns.join("\n\n")}`
|
|
2654
|
+
: undefined,
|
|
1958
2655
|
elemMaterializers.length
|
|
1959
2656
|
? `// map augmented-element copiers (pure struct field assignment — no dynamic value read).\n${elemMaterializers.join("\n\n")}`
|
|
1960
2657
|
: undefined,
|
|
@@ -1974,8 +2671,8 @@ use std::cell::RefCell;`;
|
|
|
1974
2671
|
*/
|
|
1975
2672
|
export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
1976
2673
|
const components = ir.components;
|
|
2674
|
+
const native = components.filter(isNativeRaw);
|
|
1977
2675
|
const plan = buildTypePlan(ir);
|
|
1978
|
-
const native = components.filter((c) => isNativeRaw(c, plan));
|
|
1979
2676
|
const needSync = native.some((c) => concurrencyPlan(c) !== null);
|
|
1980
2677
|
const serializers = emitSerializers(plan);
|
|
1981
2678
|
const marshallers = emitMarshallers(plan);
|
|
@@ -1986,6 +2683,160 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
1986
2683
|
const anyAsyncRunner = native.some((c) => asyncPlans.get(c.name).runnerIsAsync);
|
|
1987
2684
|
// ── per-component input decoders: generic &[(String,Value)] -> concrete InNR_<comp> (test glue). ──
|
|
1988
2685
|
const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
|
|
2686
|
+
// does any covered read de-box a named record — a componentRef record OR a map/fanout element row
|
|
2687
|
+
// (→ the scripted handler returns a ScriptedWireRow / Vec<ScriptedWireRow>)?
|
|
2688
|
+
const anyDeBox = native.some((c) => {
|
|
2689
|
+
const tn = new Map();
|
|
2690
|
+
for (const n of c.body)
|
|
2691
|
+
if (!isControlGate(n))
|
|
2692
|
+
tn.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
|
|
2693
|
+
return c.body.some((n) => {
|
|
2694
|
+
if ("cond" in n)
|
|
2695
|
+
return false;
|
|
2696
|
+
if ("map" in n || "fanout" in n)
|
|
2697
|
+
return mapFanoutElemDeBoxEligible(n, tn, plan);
|
|
2698
|
+
return deBoxEligible(tn.get(n.id));
|
|
2699
|
+
});
|
|
2700
|
+
});
|
|
2701
|
+
// scripted wire adapter (TEST glue): wraps a scripted Value as a WireRow/WireList and classifies each
|
|
2702
|
+
// attribute's native type into a DynamoDB-flavored wire tag (S/N/BOOL/L/M/NULL). A real consumer
|
|
2703
|
+
// implements WireRow over its own AttributeValue map; this drives the generated de-box from the same
|
|
2704
|
+
// scripted vectors run_behavior uses — so actualWireType observed in a mismatch is the PRODUCER tag.
|
|
2705
|
+
const wireAdapter = anyDeBox
|
|
2706
|
+
? `// ── scripted wire adapter (TEST glue) ──
|
|
2707
|
+
fn scripted_wire_tag(v: &Value) -> String {
|
|
2708
|
+
match v {
|
|
2709
|
+
Value::Null => "NULL",
|
|
2710
|
+
Value::Bool(_) => "BOOL",
|
|
2711
|
+
Value::Int(_) | Value::Float(_) => "N",
|
|
2712
|
+
Value::Str(_) => "S",
|
|
2713
|
+
Value::Arr(_) => "L",
|
|
2714
|
+
Value::Obj(_) => "M",
|
|
2715
|
+
}
|
|
2716
|
+
.to_string()
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
fn scripted_wire_raw(v: &Value) -> String {
|
|
2720
|
+
match v {
|
|
2721
|
+
Value::Null => "null".to_string(),
|
|
2722
|
+
Value::Bool(b) => b.to_string(),
|
|
2723
|
+
Value::Int(n) => n.to_string(),
|
|
2724
|
+
Value::Float(f) => f.to_string(),
|
|
2725
|
+
Value::Str(s) => s.clone(),
|
|
2726
|
+
Value::Arr(_) | Value::Obj(_) => "[composite]".to_string(),
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
fn probe_string_val(v: Option<&Value>) -> Probe<String> {
|
|
2731
|
+
match v {
|
|
2732
|
+
None => Probe::Absent,
|
|
2733
|
+
Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
|
|
2734
|
+
Some(Value::Str(s)) => Probe::Got(s.clone()),
|
|
2735
|
+
Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
fn probe_number_val(v: Option<&Value>) -> NumProbe {
|
|
2740
|
+
match v {
|
|
2741
|
+
None => NumProbe::Absent,
|
|
2742
|
+
Some(Value::Null) => NumProbe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
|
|
2743
|
+
Some(Value::Int(n)) => NumProbe::Got { raw: n.to_string(), actual_wire_type: "N".to_string() },
|
|
2744
|
+
Some(Value::Float(f)) => NumProbe::Got { raw: f.to_string(), actual_wire_type: "N".to_string() },
|
|
2745
|
+
Some(other) => NumProbe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
fn probe_bool_val(v: Option<&Value>) -> Probe<bool> {
|
|
2750
|
+
match v {
|
|
2751
|
+
None => Probe::Absent,
|
|
2752
|
+
Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
|
|
2753
|
+
Some(Value::Bool(b)) => Probe::Got(*b),
|
|
2754
|
+
Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
fn probe_row_val(v: Option<&Value>) -> Probe<ScriptedWireRow> {
|
|
2759
|
+
match v {
|
|
2760
|
+
None => Probe::Absent,
|
|
2761
|
+
Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
|
|
2762
|
+
Some(Value::Obj(pairs)) => Probe::Got(ScriptedWireRow { obj: pairs.clone() }),
|
|
2763
|
+
Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
fn probe_list_val(v: Option<&Value>) -> Probe<ScriptedWireList> {
|
|
2768
|
+
match v {
|
|
2769
|
+
None => Probe::Absent,
|
|
2770
|
+
Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
|
|
2771
|
+
Some(Value::Arr(items)) => Probe::Got(ScriptedWireList { items: items.clone() }),
|
|
2772
|
+
Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
pub struct ScriptedWireRow {
|
|
2777
|
+
obj: Vec<(String, Value)>,
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
impl ScriptedWireRow {
|
|
2781
|
+
pub fn new(v: Value) -> Self {
|
|
2782
|
+
match v {
|
|
2783
|
+
Value::Obj(pairs) => ScriptedWireRow { obj: pairs },
|
|
2784
|
+
_ => ScriptedWireRow { obj: Vec::new() },
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
fn get(&self, field: &str) -> Option<&Value> {
|
|
2788
|
+
self.obj.iter().find(|(k, _)| k.as_str() == field).map(|(_, v)| v)
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
pub struct ScriptedWireList {
|
|
2793
|
+
items: Vec<Value>,
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
impl WireRow for ScriptedWireRow {
|
|
2797
|
+
type List = ScriptedWireList;
|
|
2798
|
+
fn keys(&self) -> Vec<String> {
|
|
2799
|
+
self.obj.iter().map(|(k, _)| k.clone()).collect()
|
|
2800
|
+
}
|
|
2801
|
+
fn probe_string(&self, field: &str) -> Probe<String> {
|
|
2802
|
+
probe_string_val(self.get(field))
|
|
2803
|
+
}
|
|
2804
|
+
fn probe_number(&self, field: &str) -> NumProbe {
|
|
2805
|
+
probe_number_val(self.get(field))
|
|
2806
|
+
}
|
|
2807
|
+
fn probe_bool(&self, field: &str) -> Probe<bool> {
|
|
2808
|
+
probe_bool_val(self.get(field))
|
|
2809
|
+
}
|
|
2810
|
+
fn probe_row(&self, field: &str) -> Probe<Self> {
|
|
2811
|
+
probe_row_val(self.get(field))
|
|
2812
|
+
}
|
|
2813
|
+
fn probe_list(&self, field: &str) -> Probe<Self::List> {
|
|
2814
|
+
probe_list_val(self.get(field))
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
impl WireList for ScriptedWireList {
|
|
2819
|
+
type Row = ScriptedWireRow;
|
|
2820
|
+
fn len(&self) -> usize {
|
|
2821
|
+
self.items.len()
|
|
2822
|
+
}
|
|
2823
|
+
fn elem_string(&self, i: usize) -> Probe<String> {
|
|
2824
|
+
probe_string_val(self.items.get(i))
|
|
2825
|
+
}
|
|
2826
|
+
fn elem_number(&self, i: usize) -> NumProbe {
|
|
2827
|
+
probe_number_val(self.items.get(i))
|
|
2828
|
+
}
|
|
2829
|
+
fn elem_bool(&self, i: usize) -> Probe<bool> {
|
|
2830
|
+
probe_bool_val(self.items.get(i))
|
|
2831
|
+
}
|
|
2832
|
+
fn elem_row(&self, i: usize) -> Probe<Self::Row> {
|
|
2833
|
+
probe_row_val(self.items.get(i))
|
|
2834
|
+
}
|
|
2835
|
+
fn elem_list(&self, i: usize) -> Probe<Self> {
|
|
2836
|
+
probe_list_val(self.items.get(i))
|
|
2837
|
+
}
|
|
2838
|
+
}`
|
|
2839
|
+
: "";
|
|
1989
2840
|
// ── concrete scripted handler: implements every HandlerNR<comp> trait by decoding a scripted Value
|
|
1990
2841
|
// into the concrete per-node row (the generic scripted-source queue meets the concrete seam). ──
|
|
1991
2842
|
const decodeFns = [];
|
|
@@ -2009,22 +2860,39 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2009
2860
|
// Option<K> (NOT a boxed Value) — computed per node so the impl signature matches the trait.
|
|
2010
2861
|
const bt = boundRustType(n, typedNodes, plan);
|
|
2011
2862
|
if ("fanout" in n) {
|
|
2012
|
-
// v3: scripted fanout impl — drain one batched outcome, decode the aligned array into per-body
|
|
2013
|
-
// elem rows (a null array element decodes to the zero elem row = dangling → dropped by the arm).
|
|
2014
2863
|
const f = n.fanout;
|
|
2015
|
-
const
|
|
2864
|
+
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
2016
2865
|
const elemT = rawElemStructName(c.name, n.id);
|
|
2017
2866
|
const batchT = rawRowStructName(c.name, n.id);
|
|
2018
|
-
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2019
2867
|
const portsT = `${portsStructName(c.name, n.id)}Batch`;
|
|
2020
|
-
|
|
2021
|
-
|
|
2868
|
+
if (elemDeBox) {
|
|
2869
|
+
// a record-element fanout returns the aligned element WIRES; a null aligned body is a DANGLING
|
|
2870
|
+
// id → None (the covered runner maps it to the zero elem row).
|
|
2871
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<Option<ScriptedWireRow>>, BehaviorError> {
|
|
2872
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)}).ok_or_else(|| unknown_component(${rustStrLit(f.component)}))?;
|
|
2873
|
+
if is_err {
|
|
2874
|
+
return Err(leaf_failure(err));
|
|
2875
|
+
}
|
|
2876
|
+
let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
|
|
2877
|
+
let wires = arr
|
|
2878
|
+
.into_iter()
|
|
2879
|
+
.map(|ev| match ev {
|
|
2880
|
+
Value::Null => None,
|
|
2881
|
+
_ => Some(ScriptedWireRow::new(ev)),
|
|
2882
|
+
})
|
|
2883
|
+
.collect();
|
|
2884
|
+
Ok(wires)
|
|
2885
|
+
}`);
|
|
2886
|
+
continue;
|
|
2887
|
+
}
|
|
2888
|
+
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
2889
|
+
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2890
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<${batchT}, BehaviorError> {
|
|
2891
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)}).ok_or_else(|| unknown_component(${rustStrLit(f.component)}))?;
|
|
2022
2892
|
if is_err {
|
|
2023
|
-
return
|
|
2893
|
+
return Err(leaf_failure(err));
|
|
2024
2894
|
}
|
|
2025
2895
|
let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
|
|
2026
|
-
// v3: a null aligned body is a DANGLING id — decode it to the zero elem row (whose dedupeKey
|
|
2027
|
-
// field is empty), so the runner's dangling-drop excludes it (byte-equal to fanoutDedupDrop).
|
|
2028
2896
|
let rows = arr
|
|
2029
2897
|
.iter()
|
|
2030
2898
|
.map(|ev| match ev {
|
|
@@ -2032,7 +2900,7 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2032
2900
|
_ => ${elemDecode}(ev),
|
|
2033
2901
|
})
|
|
2034
2902
|
.collect();
|
|
2035
|
-
|
|
2903
|
+
Ok(${batchT} { rows })
|
|
2036
2904
|
}`);
|
|
2037
2905
|
continue;
|
|
2038
2906
|
}
|
|
@@ -2040,70 +2908,172 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2040
2908
|
const m = n.map;
|
|
2041
2909
|
const batched = m.batched === true;
|
|
2042
2910
|
const arrRef = ref;
|
|
2043
|
-
const
|
|
2911
|
+
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
2044
2912
|
const elemT = rawElemStructName(c.name, n.id);
|
|
2045
2913
|
const batchT = rawRowStructName(c.name, n.id);
|
|
2046
|
-
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2047
2914
|
const portsT = batched ? `${portsStructName(c.name, n.id)}Batch` : portsStructName(c.name, n.id);
|
|
2048
|
-
|
|
2915
|
+
if (elemDeBox) {
|
|
2916
|
+
// a record-element map returns the element WIRE(s); the covered runner de-boxes each strictly.
|
|
2917
|
+
if (batched) {
|
|
2918
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<ScriptedWireRow>, BehaviorError> {
|
|
2919
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
|
|
2920
|
+
if is_err {
|
|
2921
|
+
return Err(leaf_failure(err));
|
|
2922
|
+
}
|
|
2923
|
+
let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
|
|
2924
|
+
let wires = arr.into_iter().map(ScriptedWireRow::new).collect();
|
|
2925
|
+
Ok(wires)
|
|
2926
|
+
}`);
|
|
2927
|
+
}
|
|
2928
|
+
else {
|
|
2929
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<ScriptedWireRow, BehaviorError> {
|
|
2930
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
|
|
2931
|
+
if is_err {
|
|
2932
|
+
return Err(leaf_failure(err));
|
|
2933
|
+
}
|
|
2934
|
+
Ok(ScriptedWireRow::new(ok.unwrap_or(Value::Null)))
|
|
2935
|
+
}`);
|
|
2936
|
+
}
|
|
2937
|
+
continue;
|
|
2938
|
+
}
|
|
2939
|
+
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
2940
|
+
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2049
2941
|
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)})?;
|
|
2942
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<${batchT}, BehaviorError> {
|
|
2943
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
|
|
2052
2944
|
if is_err {
|
|
2053
|
-
return
|
|
2945
|
+
return Err(leaf_failure(err));
|
|
2054
2946
|
}
|
|
2055
2947
|
let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
|
|
2056
2948
|
let rows = arr.iter().map(${elemDecode}).collect();
|
|
2057
|
-
|
|
2949
|
+
Ok(${batchT} { rows })
|
|
2058
2950
|
}`);
|
|
2059
2951
|
}
|
|
2060
2952
|
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)})?;
|
|
2953
|
+
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<${elemT}, BehaviorError> {
|
|
2954
|
+
let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
|
|
2063
2955
|
if is_err {
|
|
2064
|
-
return
|
|
2956
|
+
return Err(leaf_failure(err));
|
|
2065
2957
|
}
|
|
2066
2958
|
let v = ok.unwrap_or(Value::Null);
|
|
2067
|
-
|
|
2959
|
+
Ok(${elemDecode}(&v))
|
|
2068
2960
|
}`);
|
|
2069
2961
|
}
|
|
2070
|
-
void retT;
|
|
2071
2962
|
continue;
|
|
2072
2963
|
}
|
|
2073
2964
|
const node = n;
|
|
2074
2965
|
const rowT = rawRowStructName(c.name, node.id);
|
|
2966
|
+
// a de-box-eligible node's scripted handler returns a ScriptedWireRow (the scripted Value wrapped as
|
|
2967
|
+
// a WireRow that classifies each attribute's native type into a DynamoDB-flavored wire tag); the
|
|
2968
|
+
// covered module's generated decode_wire_* then probes it. A real consumer returns its wire payload
|
|
2969
|
+
// for BC to de-box — the de-box no longer lives here.
|
|
2970
|
+
if (deBoxEligible(ref)) {
|
|
2971
|
+
// a record-containing top returns its wire (ScriptedWireRow / Vec / Option / BTreeMap) — the
|
|
2972
|
+
// covered runner de-boxes each record. #129: an arr node with an `items` port echoes the NATIVE
|
|
2973
|
+
// port array, serialized back to Vec<ScriptedWireRow> so the de-box round-trips.
|
|
2974
|
+
const top = recordTop(ref);
|
|
2975
|
+
const wireT = (() => {
|
|
2976
|
+
switch (top.kind) {
|
|
2977
|
+
case "named":
|
|
2978
|
+
return "ScriptedWireRow";
|
|
2979
|
+
case "arr":
|
|
2980
|
+
return "Vec<ScriptedWireRow>";
|
|
2981
|
+
case "opt":
|
|
2982
|
+
return "Option<ScriptedWireRow>";
|
|
2983
|
+
case "map":
|
|
2984
|
+
return "std::collections::BTreeMap<String, ScriptedWireRow>";
|
|
2985
|
+
}
|
|
2986
|
+
})();
|
|
2987
|
+
// echo-into-record (#141 fixtures): a named de-box node whose result WRAPS an input port — the
|
|
2988
|
+
// scripted leaf echoes port P into the record's single field, so the input-port value flows into
|
|
2989
|
+
// the de-boxed result and the runtime non-vacuity #139/#143/#144 assert survives the record wrap.
|
|
2990
|
+
let namedEcho = "";
|
|
2991
|
+
if (top.kind === "named") {
|
|
2992
|
+
const decl = findDecl(plan, top.innerName);
|
|
2993
|
+
const recordRust = renderTypeRef(ref);
|
|
2994
|
+
const field = decl !== undefined && decl.fields.length === 1 ? decl.fields[0] : undefined;
|
|
2995
|
+
const fieldRust = field !== undefined ? renderTypeRef(field.type) : null;
|
|
2996
|
+
for (const pn of Object.keys(node.ports)) {
|
|
2997
|
+
let portRust = null;
|
|
2998
|
+
try {
|
|
2999
|
+
portRust = portFieldRustType(node.ports[pn], c, plan, typedNodes, `echo port '${pn}'`);
|
|
3000
|
+
}
|
|
3001
|
+
catch {
|
|
3002
|
+
portRust = null;
|
|
3003
|
+
}
|
|
3004
|
+
if (portRust === recordRust) {
|
|
3005
|
+
// case 2: the port IS the whole record → echo it DIRECTLY (no field wrap), matching
|
|
3006
|
+
// run_behavior's plain echo:port of a whole-record port.
|
|
3007
|
+
const own = typeRefIsCopy(ref) ? `ports.${portFieldName(pn)}` : `ports.${portFieldName(pn)}.clone()`;
|
|
3008
|
+
namedEcho += ` if echo == ${rustStrLit(pn)} {\n let tv = ${own};\n return Ok(ScriptedWireRow::new(${serializeTyped("tv", ref, plan)}));\n }\n`;
|
|
3009
|
+
}
|
|
3010
|
+
else if (fieldRust !== null && field !== undefined && portRust === fieldRust) {
|
|
3011
|
+
// case 1: a scalar port wraps into the record's single field ({v: ports.X}).
|
|
3012
|
+
const own = typeRefIsCopy(field.type) ? `ports.${portFieldName(pn)}` : `ports.${portFieldName(pn)}.clone()`;
|
|
3013
|
+
namedEcho += ` if echo == ${rustStrLit(pn)} {\n let tv = ${own};\n return Ok(ScriptedWireRow::new(Value::Obj(vec![(${rustStrLit(field.name)}.to_string(), ${serializeTyped("tv", field.type, plan)})])));\n }\n`;
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
const canEcho = (top.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items")) || namedEcho !== "";
|
|
3018
|
+
const portsParam = canEcho ? "ports" : "_ports";
|
|
3019
|
+
const okBind = canEcho ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
|
|
3020
|
+
let body;
|
|
3021
|
+
if (top.kind === "named") {
|
|
3022
|
+
body = `${namedEcho} Ok(ScriptedWireRow::new(ok.unwrap_or(Value::Null)))`;
|
|
3023
|
+
}
|
|
3024
|
+
else if (top.kind === "arr") {
|
|
3025
|
+
const elemRef = ref.elem;
|
|
3026
|
+
const itemsF = portFieldName("items");
|
|
3027
|
+
const echoBranch = canEcho
|
|
3028
|
+
? ` if echo == "items" {\n let wires: Vec<ScriptedWireRow> = ports.${itemsF}.iter().map(|e| { let tv = e.clone(); ScriptedWireRow::new(${serializeTyped("tv", elemRef, plan)}) }).collect();\n return Ok(wires);\n }\n`
|
|
3029
|
+
: "";
|
|
3030
|
+
body = `${echoBranch} let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };\n Ok(arr.into_iter().map(ScriptedWireRow::new).collect())`;
|
|
3031
|
+
}
|
|
3032
|
+
else if (top.kind === "opt") {
|
|
3033
|
+
body = ` match ok {\n None | Some(Value::Null) => Ok(None),\n Some(v) => Ok(Some(ScriptedWireRow::new(v))),\n }`;
|
|
3034
|
+
}
|
|
3035
|
+
else {
|
|
3036
|
+
body = ` let obj = match ok { Some(Value::Obj(pairs)) => pairs, _ => Vec::new() };\n Ok(obj.into_iter().map(|(k, v)| (k, ScriptedWireRow::new(v))).collect())`;
|
|
3037
|
+
}
|
|
3038
|
+
methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Result<${wireT}, BehaviorError> {
|
|
3039
|
+
let ${okBind} = self.next(${rustStrLit(node.component)}).ok_or_else(|| unknown_component(${rustStrLit(node.component)}))?;
|
|
3040
|
+
if is_err {
|
|
3041
|
+
return Err(leaf_failure(err));
|
|
3042
|
+
}
|
|
3043
|
+
${body}
|
|
3044
|
+
}`);
|
|
3045
|
+
continue;
|
|
3046
|
+
}
|
|
2075
3047
|
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
2076
|
-
// #129 (TEST glue):
|
|
2077
|
-
//
|
|
2078
|
-
//
|
|
2079
|
-
//
|
|
2080
|
-
//
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
// a scalar/arr/opt outType row holds the port in its single `val` field.
|
|
2090
|
-
const echoRow = (p) => ref.kind === "named"
|
|
2091
|
-
? `${rowT} { ${findDecl(plan, ref.name).fields.map((f) => `${rustFieldName(f.name)}: ${own(`${portsParam}.${portFieldName(p)}.${rustFieldName(f.name)}`, f.type)}`).join(", ")}, ..Default::default() }`
|
|
2092
|
-
: `${rowT} { val: ${own(`${portsParam}.${portFieldName(p)}`, ref)}, ..Default::default() }`;
|
|
2093
|
-
const echoBranch = echoable
|
|
2094
|
-
.map((p) => `\n if echo == ${rustStrLit(p)} {\n return Some(${echoRow(p)});\n }`)
|
|
2095
|
-
.join("");
|
|
2096
|
-
const okBind = echoable.length > 0 ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
|
|
2097
|
-
methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
|
|
2098
|
-
let ${okBind} = self.next(${rustStrLit(node.component)})?;
|
|
3048
|
+
// #129 (TEST glue): if this node has an `items` port and its outType is a bare arr, support the
|
|
3049
|
+
// reference scriptedHandlers' `echo === "items"` — the scripted handler ECHOES the NATIVE `f_items`
|
|
3050
|
+
// port back as its result. This makes the node→leaf array dataflow test non-vacuous: the leaf's
|
|
3051
|
+
// observed result is exactly the native array that flowed into its ports struct, so a dropped or
|
|
3052
|
+
// boxed native array would diverge from run_behavior. `ports` is bound only on the echo path.
|
|
3053
|
+
const canEcho = ref.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items");
|
|
3054
|
+
const portsParam = canEcho ? "ports" : "_ports";
|
|
3055
|
+
const echoBranch = canEcho
|
|
3056
|
+
? `\n if echo == "items" {\n return Ok(${rowT} { val: ${portsParam}.${portFieldName("items")}.clone() });\n }`
|
|
3057
|
+
: "";
|
|
3058
|
+
const okBind = canEcho ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
|
|
3059
|
+
methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Result<${rowT}, BehaviorError> {
|
|
3060
|
+
let ${okBind} = self.next(${rustStrLit(node.component)}).ok_or_else(|| unknown_component(${rustStrLit(node.component)}))?;
|
|
2099
3061
|
if is_err {
|
|
2100
|
-
return
|
|
3062
|
+
return Err(leaf_failure(err));
|
|
2101
3063
|
}${echoBranch}
|
|
2102
3064
|
let v = ok.unwrap_or(Value::Null);
|
|
2103
|
-
|
|
3065
|
+
Ok(${decode}(&v))
|
|
2104
3066
|
}`);
|
|
2105
3067
|
}
|
|
2106
|
-
|
|
3068
|
+
const cHasDeBox = c.body.some((n) => {
|
|
3069
|
+
if ("cond" in n)
|
|
3070
|
+
return false;
|
|
3071
|
+
if ("map" in n || "fanout" in n)
|
|
3072
|
+
return mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
3073
|
+
return deBoxEligible(typedNodes.get(n.id));
|
|
3074
|
+
});
|
|
3075
|
+
const wireAssoc = cHasDeBox ? ` type Wire = ScriptedWireRow;\n` : "";
|
|
3076
|
+
traitImpls.push(`impl ${handlerTraitName(c.name)} for ScriptedNativeRaw {\n${wireAssoc}${methods.join("\n")}\n}`);
|
|
2107
3077
|
}
|
|
2108
3078
|
const arms = native
|
|
2109
3079
|
.map((c) => {
|
|
@@ -2152,7 +3122,7 @@ impl ScriptedNativeRaw {
|
|
|
2152
3122
|
}
|
|
2153
3123
|
|
|
2154
3124
|
// drain one scripted outcome for a component (last entry repeats — mirrors the reference scripted
|
|
2155
|
-
// handlers); None when the component has no scripted queue
|
|
3125
|
+
// handlers); None when the component has no scripted queue. Returns (is_error, err,
|
|
2156
3126
|
// ok_value, echo). The bc#87 parallel-stage runner calls node_* from multiple threads, so the drain
|
|
2157
3127
|
// is Mutex-guarded. #129: \`echo\` carries the reference scriptedHandlers' \`echo\` directive (e.g.
|
|
2158
3128
|
// "items") so a scripted node can ECHO a named input PORT back — used to make the node→leaf array
|
|
@@ -2162,13 +3132,11 @@ impl ScriptedNativeRaw {
|
|
|
2162
3132
|
let mut queues = self.queues.lock().unwrap();
|
|
2163
3133
|
let (queue, single) = queues.get_mut(component)?;
|
|
2164
3134
|
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(),
|
|
3135
|
+
// normalize echo:port to the port NAME X so a de-box handler's echo comparison fires;
|
|
3136
|
+
// echo:items already names the port directly.
|
|
3137
|
+
let echo = {
|
|
3138
|
+
let e = raw.get("echo").and_then(|e| e.as_str()).unwrap_or("");
|
|
3139
|
+
if e == "port" { raw.get("port").and_then(|p| p.as_str()).unwrap_or("").to_string() } else { e.to_string() }
|
|
2172
3140
|
};
|
|
2173
3141
|
if let Some(okv) = raw.get("ok") {
|
|
2174
3142
|
let v = behavior_contracts::decode_value(okv).expect("decode ok");
|
|
@@ -2196,9 +3164,27 @@ use behavior_contracts::Value;
|
|
|
2196
3164
|
use super::*;
|
|
2197
3165
|
|
|
2198
3166
|
// 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
|
|
3167
|
+
// the conformance harness reads (same code + message + structured detail) — the ONLY place the two meet
|
|
3168
|
+
// (test glue). The de-box's structured Error Value is carried over so the harness can assert it.
|
|
2200
3169
|
fn to_rt_err(e: BehaviorError) -> behavior_contracts::BehaviorError {
|
|
2201
|
-
behavior_contracts::BehaviorError::new(e.code, e.message)
|
|
3170
|
+
let mut rt = behavior_contracts::BehaviorError::new(e.code, e.message);
|
|
3171
|
+
if let Some(d) = e.detail {
|
|
3172
|
+
let d = *d;
|
|
3173
|
+
rt.detail = Some(Box::new(behavior_contracts::ErrorDetail {
|
|
3174
|
+
kind: d.kind.map(|k| match k {
|
|
3175
|
+
ErrorKind::TypeMismatch => behavior_contracts::ErrorKind::TypeMismatch,
|
|
3176
|
+
ErrorKind::MissingField => behavior_contracts::ErrorKind::MissingField,
|
|
3177
|
+
ErrorKind::Overflow => behavior_contracts::ErrorKind::Overflow,
|
|
3178
|
+
}),
|
|
3179
|
+
model: d.model,
|
|
3180
|
+
field: d.field,
|
|
3181
|
+
expected_type: d.expected_type,
|
|
3182
|
+
actual_wire_type: d.actual_wire_type,
|
|
3183
|
+
raw_value: d.raw_value,
|
|
3184
|
+
context: d.context,
|
|
3185
|
+
}));
|
|
3186
|
+
}
|
|
3187
|
+
rt
|
|
2202
3188
|
}
|
|
2203
3189
|
${anyAsyncRunner
|
|
2204
3190
|
? `// bc#97 (TEST glue) — a minimal std-only block-on that drives an async runner Future to completion so
|
|
@@ -2238,6 +3224,9 @@ ${decodeFns.join("\n\n")}
|
|
|
2238
3224
|
// input decoders (generic &[(String,Value)] -> concrete InNR_<comp>; TEST glue, one decode at the boundary).
|
|
2239
3225
|
${inDecoders}
|
|
2240
3226
|
|
|
3227
|
+
// scripted wire adapter (TEST glue) — wraps a scripted Value as a WireRow the generated de-box probes.
|
|
3228
|
+
${wireAdapter}
|
|
3229
|
+
|
|
2241
3230
|
${adapter}
|
|
2242
3231
|
|
|
2243
3232
|
// scripted handler trait impls (one per covered component — the concrete node_* seam, test glue).
|
|
@@ -2260,10 +3249,8 @@ ${arms}
|
|
|
2260
3249
|
}
|
|
2261
3250
|
/** emitInDecoder — a TEST-glue decoder `decode_InNR_<comp>(input) -> Result<InNR_<comp>, BehaviorError>`.
|
|
2262
3251
|
* 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. */
|
|
3252
|
+
* typed read; Value-typed → the raw Value clone). A missing key leaves Default (every covered input is
|
|
3253
|
+
* required + supplied by the corpus). Lives ONLY in the observe companion. */
|
|
2267
3254
|
function emitInDecoder(comp, plan) {
|
|
2268
3255
|
const name = inStructName(comp.name);
|
|
2269
3256
|
const ports = Object.keys(comp.inputPorts ?? {});
|
|
@@ -2285,15 +3272,7 @@ function emitInDecoder(comp, plan) {
|
|
|
2285
3272
|
const et = schema.elemType;
|
|
2286
3273
|
const head = i === 0 ? " if" : " } else if";
|
|
2287
3274
|
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) {
|
|
3275
|
+
if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
2297
3276
|
// #108: decode the boxed Value::Arr into the native Vec<ElemT> (test glue — materialize each elem).
|
|
2298
3277
|
const elemRef = deriveTypeRef(et, plan);
|
|
2299
3278
|
const elemTy = renderTypeRef(elemRef);
|
|
@@ -2313,6 +3292,11 @@ function emitInDecoder(comp, plan) {
|
|
|
2313
3292
|
const mapRef = { kind: "map", value: deriveTypeRef(et, plan) };
|
|
2314
3293
|
lines.push(` in_.${field} = ${materializeExpr("v", mapRef, plan)};`);
|
|
2315
3294
|
}
|
|
3295
|
+
else if (inputPortIsOptional(schema)) {
|
|
3296
|
+
// #139: an OPTIONAL scalar port decodes into Option<T> via the inputPortTypeRef SSoT (a present key
|
|
3297
|
+
// materializes Some(v); an absent key never enters this block, leaving the Default None).
|
|
3298
|
+
lines.push(` in_.${field} = ${materializeExpr("v", inputPortTypeRef(schema, plan), plan)};`);
|
|
3299
|
+
}
|
|
2316
3300
|
else if (scalar === undefined) {
|
|
2317
3301
|
lines.push(` in_.${field} = v.clone();`);
|
|
2318
3302
|
}
|
|
@@ -2331,14 +3315,6 @@ function emitInDecoder(comp, plan) {
|
|
|
2331
3315
|
lines.push(`}`);
|
|
2332
3316
|
return lines.join("\n");
|
|
2333
3317
|
}
|
|
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
3318
|
/** emitDecodeRow — emit (once, deduped) a decoder `decode_row_<comp>_<node><suffix>(v: &Value) -> <rowT>`
|
|
2343
3319
|
* that materializes the scripted ok Value into the concrete row struct. Named: reuse the Value->struct
|
|
2344
3320
|
* marshaller + move fields; scalar/arr/opt: materialize into .val. TEST glue only. */
|
|
@@ -2353,12 +3329,12 @@ function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix)
|
|
|
2353
3329
|
const decl = findDecl(plan, ref.name);
|
|
2354
3330
|
lines.push(` let t = marshal_${ref.name}(v).expect("scripted row decode");`);
|
|
2355
3331
|
const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: t.${rustFieldName(f.name)}`).join(", ");
|
|
2356
|
-
lines.push(` ${rowT} { ${inits}
|
|
3332
|
+
lines.push(` ${rowT} { ${inits} }`);
|
|
2357
3333
|
}
|
|
2358
3334
|
else {
|
|
2359
3335
|
const mat = materializeExpr("v", ref, plan);
|
|
2360
3336
|
lines.push(` let val = (|| -> Result<${renderTypeRef(ref)}, BehaviorError> { Ok(${mat}) })().expect("scripted row decode");`);
|
|
2361
|
-
lines.push(` ${rowT} { val
|
|
3337
|
+
lines.push(` ${rowT} { val }`);
|
|
2362
3338
|
}
|
|
2363
3339
|
lines.push(`}`);
|
|
2364
3340
|
out.push(lines.join("\n"));
|