behavior-contracts 0.8.4 → 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 +1306 -710
- 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 +1279 -790
- package/dist/generator/emit-typed-native-rust.js.map +1 -1
- package/dist/generator/native-expr.d.ts +23 -2
- package/dist/generator/native-expr.d.ts.map +1 -1
- package/dist/generator/native-expr.js +31 -2
- package/dist/generator/native-expr.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,250 +192,499 @@ function nodeMethodName(nodeId) {
|
|
|
78
192
|
function inStructName(compName) {
|
|
79
193
|
return `InNR${pascal(compName)}`;
|
|
80
194
|
}
|
|
81
|
-
function
|
|
82
|
-
if (
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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;
|
|
89
205
|
}
|
|
90
|
-
/**
|
|
91
|
-
*
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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,
|
|
114
255
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
case "concat":
|
|
122
|
-
return "String";
|
|
123
|
-
case "bool":
|
|
124
|
-
return "bool";
|
|
125
|
-
case "ref": {
|
|
126
|
-
// #108: a `$as`-headed ref (map element binding) — resolve the element field's scalar type.
|
|
127
|
-
if (asBinding !== undefined && e.path[0] === asBinding.name) {
|
|
128
|
-
try {
|
|
129
|
-
const { ref } = typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan);
|
|
130
|
-
if (ref.kind === "scalar" && ref.scalar !== "null")
|
|
131
|
-
return rustScalar(ref.scalar);
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
return "Value";
|
|
135
|
-
}
|
|
136
|
-
return "Value";
|
|
137
|
-
}
|
|
138
|
-
// a ref to a REQUIRED scalar input port → that scalar. An OPTIONAL port has no required-scalar
|
|
139
|
-
// kind (its native type is Option<T>) and is lowered by the opt lane ahead of this — reaching here
|
|
140
|
-
// with one means the shape is uncovered, and "Value" routes it to the caller's loud fail-closed.
|
|
141
|
-
if (e.path.length === 1) {
|
|
142
|
-
const sc = inputScalarKind(inputPorts[e.path[0]]);
|
|
143
|
-
if (sc !== undefined)
|
|
144
|
-
return sc;
|
|
145
|
-
}
|
|
146
|
-
return "Value";
|
|
147
|
-
}
|
|
148
|
-
default:
|
|
149
|
-
return "Value";
|
|
150
|
-
}
|
|
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,
|
|
151
262
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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}}`;
|
|
164
382
|
}
|
|
165
|
-
|
|
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";
|
|
166
445
|
}
|
|
167
446
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* - a string/bool/scalar-input port → the scalar Rust type (inferPortType).
|
|
173
|
-
* A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
|
|
174
|
-
* Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no `Value`
|
|
175
|
-
* fallback on the native module). `where` names the emission site for the error.
|
|
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.
|
|
176
451
|
*/
|
|
177
|
-
function
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
return ft;
|
|
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()}`;
|
|
209
482
|
}
|
|
210
483
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* type-inference and fail-closed discipline the go twin uses, so the two agree by construction):
|
|
216
|
-
*
|
|
217
|
-
* - `opt($.x)` / `$.x` at port position → `Option<T>` (the leaf receives the absent value as null,
|
|
218
|
-
* exactly as run_behavior's `evalPorts` passes it).
|
|
219
|
-
* - `coalesce(opt($.x), <literal>)` → `in_.x.unwrap_or(<literal>)` — a native `T`, no boxed Value.
|
|
220
|
-
* - `ne(opt($.x), null)` / `eq(…, null)` → the native presence test (`is_some()` / `is_none()`).
|
|
221
|
-
*
|
|
222
|
-
* Returns null when the port expression reads NO optional input (the caller keeps its existing lowering).
|
|
223
|
-
* A port that DOES read one but does not compile — or that compiles to a fallible expression, which this
|
|
224
|
-
* position cannot hoist — FAILS CLOSED loudly rather than degrade to a required type or a zero value.
|
|
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).
|
|
225
488
|
*/
|
|
226
|
-
function
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
return
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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;
|
|
235
528
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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);
|
|
240
556
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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.`);
|
|
273
605
|
}
|
|
274
|
-
|
|
275
|
-
|
|
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).`);
|
|
276
611
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (s !== undefined && s.type === "map" && e.path.length === 1) {
|
|
283
|
-
const et = s.elemType;
|
|
284
|
-
if (et !== undefined)
|
|
285
|
-
return { kind: "map", value: deriveTypeRef(et, plan) };
|
|
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.`);
|
|
286
617
|
}
|
|
287
|
-
return null;
|
|
288
618
|
}
|
|
289
|
-
if (ref === null)
|
|
290
|
-
return null;
|
|
291
|
-
// only MAP / NAMED composite fields here — scalars/arrays keep their existing dedicated paths.
|
|
292
|
-
if (ref.kind === "map" || ref.kind === "named")
|
|
293
|
-
return ref;
|
|
294
|
-
return null;
|
|
295
619
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
* ZERO boxed Value). `elemBaseExpr` is the map arm's element base (`oel_<mapId>`) for a `$as`-headed ref;
|
|
299
|
-
* omitted at the top level (only input-port composite refs resolve there). Returns null otherwise. */
|
|
300
|
-
function emitCompositePortValue(node, inputPorts, asBinding, plan, elemBaseExpr) {
|
|
301
|
-
const e = classifyExpr(node);
|
|
302
|
-
if (e.kind !== "ref")
|
|
620
|
+
function staticArrElems(node) {
|
|
621
|
+
if (node === null || typeof node !== "object" || Array.isArray(node))
|
|
303
622
|
return null;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
623
|
+
const keys = Object.keys(node);
|
|
624
|
+
if (keys.length !== 1 || keys[0] !== "arr")
|
|
625
|
+
return null;
|
|
626
|
+
const arg = node.arr;
|
|
627
|
+
return Array.isArray(arg) ? arg : null;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
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)
|
|
313
660
|
return null;
|
|
314
|
-
return
|
|
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);
|
|
315
673
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (s !== undefined && s.type === "map" && s.elemType !== undefined) {
|
|
321
|
-
return `in_.${rustFieldName(e.path[0])}.clone()`;
|
|
322
|
-
}
|
|
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}`);
|
|
323
678
|
}
|
|
324
|
-
|
|
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.
|
|
684
|
+
*/
|
|
685
|
+
function portFieldRustType(node, comp, plan, typedNodes, where = "port", opts) {
|
|
686
|
+
const c = compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), where, opts);
|
|
687
|
+
return c.renderedType ?? renderTypeRef(c.ref);
|
|
325
688
|
}
|
|
326
689
|
/**
|
|
327
690
|
* emitPortsStruct — the CONCRETE native ports struct for a componentRef node. Typed fields per the
|
|
@@ -336,7 +699,12 @@ function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
|
|
|
336
699
|
if (portNames.length === 0) {
|
|
337
700
|
return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
|
|
338
701
|
}
|
|
339
|
-
|
|
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
|
+
}
|
|
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));
|
|
340
708
|
const fields = portNames
|
|
341
709
|
.map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
|
|
342
710
|
.join("\n");
|
|
@@ -360,7 +728,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
|
|
|
360
728
|
// #108: element ports may read the `$as` binding — resolve the over element type for native typing.
|
|
361
729
|
const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
|
|
362
730
|
const asBinding = { name: m.as, ref: elemRef, plan };
|
|
363
|
-
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan);
|
|
731
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan, typedNodes);
|
|
364
732
|
return `${elemStruct}
|
|
365
733
|
|
|
366
734
|
// ${batch} — CONCRETE batched ports for map '${node.id}': the Vec of per-element CONCRETE ports structs
|
|
@@ -378,7 +746,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
|
|
|
378
746
|
const batch = `${name}Batch`;
|
|
379
747
|
const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
|
|
380
748
|
const asBinding = { name: f.as, ref: elemRef, plan };
|
|
381
|
-
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan);
|
|
749
|
+
const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan, typedNodes);
|
|
382
750
|
return `${elemStruct}
|
|
383
751
|
|
|
384
752
|
// ${batch} — CONCRETE batched ports for fanout '${node.id}': the Vec of per-id CONCRETE ports structs.
|
|
@@ -398,6 +766,20 @@ let rustExprTempSeq = 0;
|
|
|
398
766
|
/** module-level accumulator: set when the native-expr compiler emits a fallible helper call (so emit()
|
|
399
767
|
* bakes the helper library). Reset at the start of each emit(). */
|
|
400
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
|
+
}
|
|
401
783
|
/** makeRustExprBackend — renders native-expr for Rust. `resolveHead` maps a ref head to an OWNED typed
|
|
402
784
|
* base expr (a prior-node cell clone / an input field / a $as element). Sets rustExprUsed.helpers when
|
|
403
785
|
* a fallible helper is emitted. The runner is Result<_, BehaviorError>, so hoistFallible uses `?`. */
|
|
@@ -441,44 +823,39 @@ function makeRustExprBackend(resolveHead, plan) {
|
|
|
441
823
|
notOp: (a) => `(!${a})`,
|
|
442
824
|
structLit: (name, inits) => `${name} { ${inits.map((i) => `${rustFieldName(i.field)}: ${i.expr}`).join(", ")} }`,
|
|
443
825
|
arrLit: (elemTy, items) => `{ let __v: Vec<${elemTy}> = vec![${items.join(", ")}]; __v }`,
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
// opt (
|
|
448
|
-
//
|
|
449
|
-
// resolver adds so the value can be moved into a port) is dropped here — testing `Option<String>`
|
|
450
|
-
// must not heap-allocate. Mirrors the go twin's zero-cost `!= nil`.
|
|
451
|
-
optIsSome: (expr) => `${rustStripTrailingClone(expr)}.is_some()`,
|
|
452
|
-
optIsNone: (expr) => `${rustStripTrailingClone(expr)}.is_none()`,
|
|
453
|
-
// opt (`Option<T>`) defaulted to a PURE default — it cannot fail and has no effect, so its
|
|
454
|
-
// evaluation order is UNOBSERVABLE and both forms below are correct (a FALLIBLE default, where
|
|
455
|
-
// laziness IS observable, goes to optCoalesceGuard instead). `unwrap_or` is EAGER — fine for a plain
|
|
456
|
-
// literal/copy. A default that allocates (an owned `String`, a struct/vec literal) uses
|
|
457
|
-
// `unwrap_or_else`, whose closure defers the allocation to the absent branch: not a semantic
|
|
458
|
-
// requirement here, but pointless work otherwise — and what clippy `or_fun_call` asks for.
|
|
826
|
+
// a projection port of static string literals — `vec!["a", "b"]` infers `&'static str` (the field is
|
|
827
|
+
// Vec<&'static str>), ZERO heap allocation.
|
|
828
|
+
staticStrArr: (literals) => ({ expr: `vec![${literals.map((s) => rustStrLit(s)).join(", ")}]`, type: "Vec<&'static str>" }),
|
|
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).
|
|
459
831
|
optUnwrapOr: (optExpr, defaultExpr, _innerTy) => /^[A-Za-z0-9_.:]+$/.test(defaultExpr) ? `${optExpr}.unwrap_or(${defaultExpr})` : `${optExpr}.unwrap_or_else(|| ${defaultExpr})`,
|
|
460
832
|
// opt defaulted to a FALLIBLE default — a `match`, NOT a closure: the default's hoisted `?` early
|
|
461
|
-
// returns must propagate out of the ENCLOSING fn
|
|
462
|
-
// (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.
|
|
463
834
|
optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
|
|
464
835
|
`let ${temp}: ${ty} = match ${optExpr} {`,
|
|
465
836
|
` Some(${bindTemp}) => ${bindTemp},`,
|
|
466
837
|
` None => { ${rustBlockBody(dStmts, dExpr)} }`,
|
|
467
838
|
`};`,
|
|
468
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()`,
|
|
469
846
|
ternary: (cond, t, e, _ty) => `(if ${rustStripOuterParens(cond)} { ${rustStripOuterParens(t)} } else { ${rustStripOuterParens(e)} })`,
|
|
470
847
|
shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
|
|
471
848
|
// and: let temp = if left { <rStmts> rExpr } else { false }; or: if left { true } else { <rStmts> rExpr }.
|
|
472
849
|
// build a block that runs rStmts then yields rExpr (parens stripped — block-tail is not a sub-expr).
|
|
473
|
-
const rblock = `{ ${
|
|
850
|
+
const rblock = `{ ${rStmts.join(" ")} ${rustStripOuterParens(rExpr)} }`;
|
|
474
851
|
if (op === "and") {
|
|
475
852
|
return [`let ${temp}: bool = if ${rustStripOuterParens(left)} ${rblock} else { false };`];
|
|
476
853
|
}
|
|
477
854
|
return [`let ${temp}: bool = if ${rustStripOuterParens(left)} { true } else ${rblock};`];
|
|
478
855
|
},
|
|
479
856
|
condGuard: (temp, ty, cond, tStmts, tExpr, eStmts, eExpr) => {
|
|
480
|
-
const tblock = `{ ${
|
|
481
|
-
const eblock = `{ ${
|
|
857
|
+
const tblock = `{ ${tStmts.join(" ")} ${rustStripOuterParens(tExpr)} }`;
|
|
858
|
+
const eblock = `{ ${eStmts.join(" ")} ${rustStripOuterParens(eExpr)} }`;
|
|
482
859
|
return [`let ${temp}: ${ty} = if ${rustStripOuterParens(cond)} ${tblock} else ${eblock};`];
|
|
483
860
|
},
|
|
484
861
|
};
|
|
@@ -487,17 +864,6 @@ function makeRustExprBackend(resolveHead, plan) {
|
|
|
487
864
|
function rustSnake(name) {
|
|
488
865
|
return name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").replace(/([0-9])([a-z])/g, "$1_$2").toLowerCase();
|
|
489
866
|
}
|
|
490
|
-
/** rustBlockBody — the body of a block-expression that runs `stmts` then yields `expr`. A trailing
|
|
491
|
-
* `let t = E; t` is collapsed to `E`: binding a temp only to return it on the next line is clippy's
|
|
492
|
-
* `let_and_return` (an error under `-D warnings`), and the hoisted-fallible shape produces exactly that
|
|
493
|
-
* whenever the block's tail IS the hoisted temp. */
|
|
494
|
-
function rustBlockBody(stmts, expr) {
|
|
495
|
-
const last = stmts[stmts.length - 1];
|
|
496
|
-
const m = last !== undefined ? /^let\s+([A-Za-z0-9_]+)(?::\s*[^=]+)?\s*=\s*(.+);$/.exec(last) : null;
|
|
497
|
-
if (m !== null && m[1] === expr)
|
|
498
|
-
return [...stmts.slice(0, -1), m[2]].join(" ");
|
|
499
|
-
return [...stmts, rustStripOuterParens(expr)].join(" ");
|
|
500
|
-
}
|
|
501
867
|
/** strip ONE redundant outer paren pair from an expr (clippy `if (cond)` unused_parens). Only when the
|
|
502
868
|
* whole string is a single balanced `( … )` — a `(a) && (b)` stays untouched. */
|
|
503
869
|
function rustStripOuterParens(expr) {
|
|
@@ -515,12 +881,6 @@ function rustStripOuterParens(expr) {
|
|
|
515
881
|
}
|
|
516
882
|
return expr.slice(1, -1);
|
|
517
883
|
}
|
|
518
|
-
/** Drop a TRAILING `.clone()` from an expression — for a position that only reads the value (no move),
|
|
519
|
-
* so an owning clone added by a head resolver is pure waste. Only a clone at the very END is dropped: a
|
|
520
|
-
* `.clone()` mid-path (`t_x.borrow().clone().field`) still owns the field access that follows it. */
|
|
521
|
-
function rustStripTrailingClone(expr) {
|
|
522
|
-
return expr.endsWith(".clone()") ? expr.slice(0, -".clone()".length) : expr;
|
|
523
|
-
}
|
|
524
884
|
/** Rust float literal (finite; integral gets `.0`). */
|
|
525
885
|
function rustFloatLitExpr(n) {
|
|
526
886
|
if (Number.isInteger(n))
|
|
@@ -614,13 +974,14 @@ fn bc_expr_str_ge(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) != std::cmp::
|
|
|
614
974
|
// ── CONCRETE per-node row structs + row→outType copiers (mirror the Go concrete contract) ──────────
|
|
615
975
|
//
|
|
616
976
|
// Each covered node has a CONCRETE row struct `RawRowNR<Comp><Node>` whose fields are the node's projected
|
|
617
|
-
// 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
|
|
618
979
|
// flattens to one Rust field per outType field; a scalar/arr/opt node carries a single `val: <typed>`
|
|
619
980
|
// payload. The per-component `HandlerNR<Comp>` trait has one method per node returning that concrete
|
|
620
981
|
// struct; the runner reads `row.<field>` DIRECTLY (no `RawValue`, no `match`, no `Value::`) and copies
|
|
621
982
|
// each field into the node's outType struct local.
|
|
622
983
|
/** emitRawRowStructs — emit the concrete `RawRowNR<Comp><Node>` struct per covered node (+ per-element
|
|
623
|
-
* `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. */
|
|
624
985
|
function emitRawRowStructs(comp, typedNodes, plan) {
|
|
625
986
|
const parts = [];
|
|
626
987
|
for (const n of comp.body) {
|
|
@@ -631,7 +992,7 @@ function emitRawRowStructs(comp, typedNodes, plan) {
|
|
|
631
992
|
const elemRowRef = fanoutItemsElemRef(n, ref, plan);
|
|
632
993
|
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
633
994
|
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
|
|
634
|
-
`#[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}`);
|
|
635
996
|
continue;
|
|
636
997
|
}
|
|
637
998
|
if ("map" in n) {
|
|
@@ -639,22 +1000,20 @@ function emitRawRowStructs(comp, typedNodes, plan) {
|
|
|
639
1000
|
const elemRowRef = mapElemRowRef(n, arrRef, plan);
|
|
640
1001
|
parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
|
|
641
1002
|
parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
|
|
642
|
-
`#[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}`);
|
|
643
1004
|
continue;
|
|
644
1005
|
}
|
|
645
1006
|
parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
|
|
646
1007
|
}
|
|
647
1008
|
return parts.join("\n\n");
|
|
648
1009
|
}
|
|
649
|
-
/** Emit one concrete row struct: flatten a named outType to typed fields, else a single `val` payload
|
|
650
|
-
*
|
|
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. */
|
|
651
1012
|
function emitOneRowStruct(name, ref, plan) {
|
|
652
1013
|
const lines = [];
|
|
653
|
-
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).`);
|
|
654
1015
|
lines.push(`#[derive(Default)]`);
|
|
655
1016
|
lines.push(`pub struct ${name} {`);
|
|
656
|
-
lines.push(` pub is_error: bool,`);
|
|
657
|
-
lines.push(` pub err: String,`);
|
|
658
1017
|
if (ref.kind === "named") {
|
|
659
1018
|
const decl = findDecl(plan, ref.name);
|
|
660
1019
|
for (const f of decl.fields) {
|
|
@@ -729,6 +1088,19 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
729
1088
|
lines.push(`// / boxed-value / dynamic accessor crosses the covered boundary — the consumer implements each`);
|
|
730
1089
|
lines.push(`// node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
|
|
731
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;`);
|
|
732
1104
|
for (const n of comp.body) {
|
|
733
1105
|
if ("cond" in n)
|
|
734
1106
|
continue; // #108: a cond node has no handler method (pure Expression).
|
|
@@ -736,21 +1108,29 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
736
1108
|
const fnKw = fnKwFor(n.id);
|
|
737
1109
|
const bt = boundRustType(n, typedNodes, plan);
|
|
738
1110
|
if ("fanout" in n) {
|
|
739
|
-
// 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).
|
|
740
1114
|
const portsT = `${portsStructName(comp.name, n.id)}Batch`;
|
|
741
|
-
const
|
|
742
|
-
|
|
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>;`);
|
|
743
1118
|
continue;
|
|
744
1119
|
}
|
|
745
1120
|
if ("map" in n) {
|
|
746
1121
|
const m = n.map;
|
|
747
1122
|
const batched = m.batched === true;
|
|
748
1123
|
const portsT = batched ? `${portsStructName(comp.name, n.id)}Batch` : portsStructName(comp.name, n.id);
|
|
749
|
-
|
|
750
|
-
|
|
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>;`);
|
|
751
1128
|
continue;
|
|
752
1129
|
}
|
|
753
|
-
|
|
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>;`);
|
|
754
1134
|
}
|
|
755
1135
|
lines.push(`}`);
|
|
756
1136
|
return lines.join("\n");
|
|
@@ -763,18 +1143,16 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
|
|
|
763
1143
|
function mapElemMaterializerName(compName, nodeId) {
|
|
764
1144
|
return `materialize_map_elem_${sanitize(compName)}_${typedCell(nodeId)}`;
|
|
765
1145
|
}
|
|
766
|
-
/** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
* 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))`). */
|
|
774
1153
|
function mapNodeArrRef(node, plan) {
|
|
775
1154
|
const outT = node.outType;
|
|
776
|
-
|
|
777
|
-
return deriveTypeRef((isArr ? outT : { arr: outT }), plan);
|
|
1155
|
+
return deriveTypeRef({ arr: outT }, plan);
|
|
778
1156
|
}
|
|
779
1157
|
/** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
|
|
780
1158
|
* type (into) or the element outType directly (no-into). This is the type of RawElemNR<Comp><Node>. */
|
|
@@ -869,44 +1247,6 @@ function mapOverElemInfo(comp, node, typedNodes, plan) {
|
|
|
869
1247
|
function mapOverElemType(comp, node, typedNodes, plan) {
|
|
870
1248
|
return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
|
|
871
1249
|
}
|
|
872
|
-
/**
|
|
873
|
-
* priorNodeArrElemInfo (#129, rust twin) — resolve a leaf port that is a `ref` into a PRIOR BODY NODE
|
|
874
|
-
* whose annotated `outType` is (or reaches, through a field path) a native array, to its element TypeRef
|
|
875
|
-
* + the OWNED Rust Vec expression that yields it. GENERALIZES the `map…over` prior-node-arr dataflow
|
|
876
|
-
* (mapOverElemInfo's first branch) to ANY leaf input port: a fold/dedup leaf whose `items` port is
|
|
877
|
-
* `{ref:["<priorNode>"]}` (or `.items` of a prior connection node) now lowers to `Vec<ElemT>` cloned
|
|
878
|
-
* out of the prior node's typed cell — ZERO boxed Value on the read hot path.
|
|
879
|
-
*
|
|
880
|
-
* The element type comes STRICTLY from the prior node's EXPLICIT `outType` annotation walked through
|
|
881
|
-
* typedFieldAccess (BC does NOT infer — consumer-interface C3 / [[graphddb-typed-native-static-link]]):
|
|
882
|
-
* if the head is not a prior body node, or its outType (through the field path) is not an `arr`, this
|
|
883
|
-
* returns null and the caller keeps its existing fail-closed behaviour (never a silent box). A
|
|
884
|
-
* single-segment INPUT-array port (elemType) is a DIFFERENT covered shape (portArrayElemRef) — handled
|
|
885
|
-
* ahead of this. Requires the typedNodes map (prior-node outType TypeRefs) and a TypePlan.
|
|
886
|
-
*/
|
|
887
|
-
function priorNodeArrElemInfo(node, typedNodes, plan) {
|
|
888
|
-
if (typedNodes === undefined || plan === undefined)
|
|
889
|
-
return null;
|
|
890
|
-
if (staticArrElems(node) !== null)
|
|
891
|
-
return null; // a static array literal — not a prior-node ref.
|
|
892
|
-
const e = classifyExpr(node);
|
|
893
|
-
if (e.kind !== "ref" || e.opt || e.path.length < 1)
|
|
894
|
-
return null;
|
|
895
|
-
if (!typedNodes.has(e.path[0]))
|
|
896
|
-
return null; // head is not a prior body node → not this shape.
|
|
897
|
-
const baseRef = typedNodes.get(e.path[0]);
|
|
898
|
-
let acc;
|
|
899
|
-
try {
|
|
900
|
-
acc = typedFieldAccess(`${typedCell(e.path[0])}.borrow()`, baseRef, e.path.slice(1), plan);
|
|
901
|
-
}
|
|
902
|
-
catch {
|
|
903
|
-
return null; // the field path does not resolve on the prior node's outType → fail-closed at caller.
|
|
904
|
-
}
|
|
905
|
-
if (acc.ref.kind !== "arr")
|
|
906
|
-
return null; // the prior-node result (through the path) is not an array.
|
|
907
|
-
// OWNED: clone the borrowed slice out of the RefCell so the ports struct owns its Vec<ElemT>.
|
|
908
|
-
return { elemRef: acc.ref.elem, overExpr: `${acc.expr}.clone()` };
|
|
909
|
-
}
|
|
910
1250
|
// ── eligibility ─────────────────────────────────────────────────────────────────────
|
|
911
1251
|
function reconstructR(e) {
|
|
912
1252
|
switch (e.kind) {
|
|
@@ -947,31 +1287,46 @@ function numLiteralRustExpr(node) {
|
|
|
947
1287
|
// fractional / exponent literal → checked f64 (finite, guaranteed above).
|
|
948
1288
|
return `Value::Float(${node}f64)`;
|
|
949
1289
|
}
|
|
950
|
-
/** A port node is static (literal / ref / concat of those / static arr / bare number literal
|
|
951
|
-
*
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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. */
|
|
1298
|
+
function buildTypedNodes(comp, plan) {
|
|
1299
|
+
const typedNodes = new Map();
|
|
1300
|
+
for (const n of comp.body) {
|
|
1301
|
+
if (isControlGate(n))
|
|
1302
|
+
continue;
|
|
1303
|
+
try {
|
|
1304
|
+
typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
|
|
1305
|
+
}
|
|
1306
|
+
catch {
|
|
1307
|
+
/* untyped node — omit */
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return typedNodes;
|
|
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);
|
|
963
1326
|
return true;
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
case "bool":
|
|
968
|
-
case "null":
|
|
969
|
-
case "ref":
|
|
970
|
-
return true;
|
|
971
|
-
case "concat":
|
|
972
|
-
return e.parts.every((p) => portIsStatic(reconstructR(p), inputPorts));
|
|
973
|
-
default:
|
|
974
|
-
return false;
|
|
1327
|
+
}
|
|
1328
|
+
catch {
|
|
1329
|
+
return false;
|
|
975
1330
|
}
|
|
976
1331
|
}
|
|
977
1332
|
/** The output must lower to a typed struct/value with no scope read: a ref to a BODY NODE, an
|
|
@@ -1037,8 +1392,11 @@ function coveredMapNode(n, bodyIds, comp) {
|
|
|
1037
1392
|
return false;
|
|
1038
1393
|
}
|
|
1039
1394
|
const ports = m.ports;
|
|
1395
|
+
const plan = planForComp(comp);
|
|
1396
|
+
const typedNodes = buildTypedNodes(comp, plan);
|
|
1397
|
+
const asBinding = mapAsBinding(comp, n, typedNodes, plan);
|
|
1040
1398
|
for (const p of Object.keys(ports))
|
|
1041
|
-
if (!portIsStatic(ports[p], comp
|
|
1399
|
+
if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
|
|
1042
1400
|
return false;
|
|
1043
1401
|
return true;
|
|
1044
1402
|
}
|
|
@@ -1064,8 +1422,17 @@ function coveredFanoutNode(n, bodyIds, comp) {
|
|
|
1064
1422
|
return false;
|
|
1065
1423
|
}
|
|
1066
1424
|
const ports = f.ports;
|
|
1425
|
+
const plan = planForComp(comp);
|
|
1426
|
+
const typedNodes = buildTypedNodes(comp, plan);
|
|
1427
|
+
let asBinding;
|
|
1428
|
+
try {
|
|
1429
|
+
asBinding = { name: f.as, ref: fanoutOverElemInfo(comp, n, typedNodes, plan).elemRef, plan };
|
|
1430
|
+
}
|
|
1431
|
+
catch {
|
|
1432
|
+
asBinding = undefined;
|
|
1433
|
+
}
|
|
1067
1434
|
for (const p of Object.keys(ports))
|
|
1068
|
-
if (!portIsStatic(ports[p], comp
|
|
1435
|
+
if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
|
|
1069
1436
|
return false;
|
|
1070
1437
|
return true;
|
|
1071
1438
|
}
|
|
@@ -1112,6 +1479,8 @@ function isSequentialComponentRefRead(comp) {
|
|
|
1112
1479
|
* can never disagree. This is the message the fail-closed dispatch surfaces per non-native component.
|
|
1113
1480
|
*/
|
|
1114
1481
|
function coverageRejectReason(comp) {
|
|
1482
|
+
const crPlan = planForComp(comp);
|
|
1483
|
+
const crTypedNodes = buildTypedNodes(comp, crPlan);
|
|
1115
1484
|
const ep = execPlan(comp);
|
|
1116
1485
|
if (ep === null)
|
|
1117
1486
|
return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
|
|
@@ -1176,7 +1545,7 @@ function coverageRejectReason(comp) {
|
|
|
1176
1545
|
if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
|
|
1177
1546
|
return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
|
|
1178
1547
|
for (const p of Object.keys(cr.ports))
|
|
1179
|
-
if (portIsStatic(cr.ports[p], comp
|
|
1548
|
+
if (portIsStatic(cr.ports[p], comp, crPlan, crTypedNodes) === false)
|
|
1180
1549
|
return `node '${n.id}' port '${p}' is not statically resolvable`;
|
|
1181
1550
|
}
|
|
1182
1551
|
if (!outputIsNativeLowerable(comp))
|
|
@@ -1204,31 +1573,17 @@ function assertTypedCoverage(comp) {
|
|
|
1204
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).`);
|
|
1205
1574
|
}
|
|
1206
1575
|
// ── typed input struct (InNR<Comp>) + NATIVE scalar port lowering ────────────────────────────
|
|
1207
|
-
/**
|
|
1208
|
-
* inputPortRustType — the native Rust type for an input port's declared type. Scalars lower to the
|
|
1209
|
-
* concrete scalar; `array`/`map` with a declared elemType to `Vec<ElemT>` / `BTreeMap<String, ElemT>`;
|
|
1210
|
-
* an OPTIONAL port (`{opt:T}` → `required:false`) to `Option<T>` — the native representation of "the
|
|
1211
|
-
* value is absent". A declared type with no native lowering stays the boxed `Value`.
|
|
1212
|
-
*
|
|
1213
|
-
* An OPTIONAL port whose inner type has no native lowering FAILS CLOSED: `Option<Value>` would be a
|
|
1214
|
-
* boxed escape, and emitting the bare inner type would silently turn "absent" into a zero value. A port
|
|
1215
|
-
* whose optionality cannot be represented is never silently coerced.
|
|
1216
|
-
*/
|
|
1576
|
+
/** Rust type for an input port's declared type (scalar → concrete; array+elemType → Vec<ElemT>; else Value). */
|
|
1217
1577
|
function inputPortRustType(schema, plan, where = "input port") {
|
|
1218
1578
|
const ref = inputPortTypeRef(schema, plan);
|
|
1219
1579
|
if (ref !== undefined)
|
|
1220
1580
|
return renderTypeRef(ref);
|
|
1221
1581
|
if (inputPortIsOptional(schema))
|
|
1222
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Rust lowering, so "absent" has no native representation
|
|
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.`);
|
|
1223
1583
|
return "Value";
|
|
1224
1584
|
}
|
|
1225
|
-
/** The scalar kind
|
|
1226
|
-
* An OPTIONAL port is deliberately NOT a scalar kind here: its native type is `Option<T>`, and the
|
|
1227
|
-
* callers of this function build REQUIRED scalar slots (`in_.<field>` read straight into a non-opt port
|
|
1228
|
-
* field). Answering "i64" for an `{opt:"int"}` port is exactly how "absent" would silently become `0`. */
|
|
1585
|
+
/** The scalar kind an input port declares, or undefined if it is not a native scalar. */
|
|
1229
1586
|
function inputScalarKind(schema) {
|
|
1230
|
-
if (inputPortIsOptional(schema))
|
|
1231
|
-
return undefined;
|
|
1232
1587
|
switch (schema?.type) {
|
|
1233
1588
|
case "string":
|
|
1234
1589
|
case "literal": // a `"literal"` union is a constrained String (see inputPortRustType).
|
|
@@ -1252,6 +1607,8 @@ function emitInStruct(comp, plan) {
|
|
|
1252
1607
|
if (ports.length === 0) {
|
|
1253
1608
|
return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\n#[derive(Default)]\npub struct ${name};`;
|
|
1254
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).
|
|
1255
1612
|
const fields = ports
|
|
1256
1613
|
.map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`)}, // ${JSON.stringify(p)}`)
|
|
1257
1614
|
.join("\n");
|
|
@@ -1262,64 +1619,10 @@ pub struct ${name} {
|
|
|
1262
1619
|
${fields}
|
|
1263
1620
|
}`;
|
|
1264
1621
|
}
|
|
1265
|
-
/**
|
|
1266
|
-
*
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
* emitNativeScalar — lower a static port expr to a NATIVE Rust expression of the concrete `want` scalar
|
|
1270
|
-
* type, or null if it cannot be statically proven that type. A string literal is native for
|
|
1271
|
-
* `want==="string"`, a bool literal for `want==="bool"`. A ref resolves via `resolveRef`. A concat lowers
|
|
1272
|
-
* ONLY into `string` and only when EVERY part is a native string, emitting a single `format!("{}{}…", …)`.
|
|
1273
|
-
*/
|
|
1274
|
-
function emitNativeScalar(node, want, resolveRef) {
|
|
1275
|
-
if (staticArrElems(node) !== null)
|
|
1276
|
-
return null;
|
|
1277
|
-
const e = classifyExpr(node);
|
|
1278
|
-
switch (e.kind) {
|
|
1279
|
-
case "str":
|
|
1280
|
-
return want === "String" ? `${rustStrLit(e.value)}.to_string()` : null;
|
|
1281
|
-
case "bool":
|
|
1282
|
-
return want === "bool" ? (e.value ? "true" : "false") : null;
|
|
1283
|
-
case "null":
|
|
1284
|
-
return null;
|
|
1285
|
-
case "ref": {
|
|
1286
|
-
const sc = resolveRef(e.path[0], e.path.slice(1), e.opt);
|
|
1287
|
-
if (sc === null || sc.scalar !== want)
|
|
1288
|
-
return null;
|
|
1289
|
-
return sc.expr;
|
|
1290
|
-
}
|
|
1291
|
-
case "concat": {
|
|
1292
|
-
if (want !== "String")
|
|
1293
|
-
return null;
|
|
1294
|
-
// a single-part concat is just the owned String of that part.
|
|
1295
|
-
if (e.parts.length === 1)
|
|
1296
|
-
return emitNativeScalar(reconstructR(e.parts[0]), "String", resolveRef);
|
|
1297
|
-
// multi-part: fold into ONE format! (statically strings only — the concat's strings-only
|
|
1298
|
-
// TYPE_MISMATCH can never fire, so dropping the check is sound). To stay clippy-clean
|
|
1299
|
-
// (to_string_in_format_args), a string-literal part is baked into the format STRING itself
|
|
1300
|
-
// (escaped) rather than passed as a `{}` arg; a ref part becomes a `{}` arg (Display on String).
|
|
1301
|
-
let fmt = "";
|
|
1302
|
-
const args = [];
|
|
1303
|
-
for (const p of e.parts) {
|
|
1304
|
-
const sub = classifyExpr(reconstructR(p));
|
|
1305
|
-
if (sub.kind === "str") {
|
|
1306
|
-
fmt += sub.value.replace(/[{}]/g, (c) => c + c); // escape { } for the format string
|
|
1307
|
-
continue;
|
|
1308
|
-
}
|
|
1309
|
-
const arg = emitNativeScalar(reconstructR(p), "String", resolveRef);
|
|
1310
|
-
if (arg === null)
|
|
1311
|
-
return null;
|
|
1312
|
-
fmt += "{}";
|
|
1313
|
-
args.push(arg);
|
|
1314
|
-
}
|
|
1315
|
-
if (args.length === 0)
|
|
1316
|
-
return `${rustStrLit(fmt)}.to_string()`;
|
|
1317
|
-
return `format!(${rustStrLit(fmt)}, ${args.join(", ")})`;
|
|
1318
|
-
}
|
|
1319
|
-
default:
|
|
1320
|
-
return null;
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
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. */
|
|
1323
1626
|
// bc#90 / runtime-free: the old `emitStaticR` / `inputLeafR` / `valueLeafR` port lowering (which built
|
|
1324
1627
|
// `Value` leaves via the primitives::* helpers and struct-field serializers) is REMOVED — every covered
|
|
1325
1628
|
// port now lowers to a FULLY NATIVE Rust value in emitPortInit (string / bool / static-string-array /
|
|
@@ -1336,100 +1639,13 @@ function emitNativeScalar(node, want, resolveRef) {
|
|
|
1336
1639
|
* a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
|
|
1337
1640
|
* ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
|
|
1338
1641
|
*/
|
|
1339
|
-
function emitPortInit(pn, expr,
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
if (optPort !== null)
|
|
1345
|
-
return `${field}: ${optPort.expr}`;
|
|
1346
|
-
// #108-map: a port that is a ref to a `$as` element MAP/NAMED field (or an input map port) → CLONE the
|
|
1347
|
-
// typed composite value directly off the element/input (native BTreeMap / struct — ZERO boxed Value).
|
|
1348
|
-
// Reuses portCompositeRef for the type; the value is the typed field access `.clone()`.
|
|
1349
|
-
if (plan !== undefined) {
|
|
1350
|
-
const compExpr = emitCompositePortValue(expr, inputPorts ?? {}, asBinding, plan, elemBaseExpr);
|
|
1351
|
-
if (compExpr !== null)
|
|
1352
|
-
return `${field}: ${compExpr}`;
|
|
1353
|
-
}
|
|
1354
|
-
// #110: a componentRef port bound to a runtime array input port (with elemType) → the native Vec<ElemT>,
|
|
1355
|
-
// cloned out of the concrete input struct (`in_.<field>.clone()`) — ZERO boxed Value on the read hot path.
|
|
1356
|
-
if (inputPorts !== undefined && portArrayElemRef(expr, inputPorts, plan) !== null) {
|
|
1357
|
-
const e = classifyExpr(expr);
|
|
1358
|
-
if (e.kind === "ref" && e.path.length === 1) {
|
|
1359
|
-
return `${field}: in_.${rustFieldName(e.path[0])}.clone()`;
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
// #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) →
|
|
1363
|
-
// the native Vec<ElemT> cloned off the prior node's typed cell (`t_<node>.borrow().field….clone()`) —
|
|
1364
|
-
// ZERO boxed Value on the read hot path (a fold/dedup leaf whose `items` = a prior node's array result).
|
|
1365
|
-
const priorArr = priorNodeArrElemInfo(expr, typedNodes, plan);
|
|
1366
|
-
if (priorArr !== null)
|
|
1367
|
-
return `${field}: ${priorArr.overExpr}`;
|
|
1368
|
-
// change #2: a static string array (projection) → a native `vec![...]` of &'static str.
|
|
1369
|
-
const strArr = staticStringArrElems(expr);
|
|
1370
|
-
if (strArr !== null) {
|
|
1371
|
-
if (fieldRustType !== "Vec<&'static str>") {
|
|
1372
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90): port '${pn}' is a static string array but its field type is '${fieldRustType}' (expected Vec<&'static str>).`);
|
|
1373
|
-
}
|
|
1374
|
-
return `${field}: vec![${strArr.map((s) => rustStrLit(s)).join(", ")}]`;
|
|
1375
|
-
}
|
|
1376
|
-
// a bare number literal (e.g. Query `limit: 20`): the SSoT range check ran in numLiteralRustExpr —
|
|
1377
|
-
// emit the concrete native i64/f64 (unwrap the Value::Int/Float box: it was only there for the old
|
|
1378
|
-
// boxed path; the range check is what matters, and it already passed).
|
|
1379
|
-
const num = numLiteralRustExpr(expr);
|
|
1380
|
-
if (num !== null) {
|
|
1381
|
-
if (fieldRustType === "i64")
|
|
1382
|
-
return `${field}: ${expr}i64`;
|
|
1383
|
-
if (fieldRustType === "f64")
|
|
1384
|
-
return `${field}: ${expr}f64`;
|
|
1385
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90): port '${pn}' is a number literal but its field type is '${fieldRustType}'.`);
|
|
1386
|
-
}
|
|
1387
|
-
// string / bool / scalar-input / concat → native scalar expression (no Value, no `match`).
|
|
1388
|
-
const native = emitNativeScalar(expr, fieldRustType, resolveRef);
|
|
1389
|
-
if (native !== null) {
|
|
1390
|
-
return `${field}: ${native}`;
|
|
1391
|
-
}
|
|
1392
|
-
// FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would re-couple
|
|
1393
|
-
// the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the component is not
|
|
1394
|
-
// native-eligible and must regenerate on the boxed straight-line path.
|
|
1395
|
-
throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#90 / runtime-free): port '${pn}' (${JSON.stringify(expr)}) does not lower to a native Rust value of type '${fieldRustType}'. The covered read plane admits NO boxed Value escape. ${RUST_STATIC_PORT_FORMS} Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
|
|
1642
|
+
function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
|
|
1643
|
+
const c = compilePortNode(expr, comp, plan, typedNodes, isPrior, `port '${pn}'`, opts);
|
|
1644
|
+
if (c.stmts.length > 0)
|
|
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.`);
|
|
1646
|
+
return `${portFieldName(pn)}: ${c.expr}`;
|
|
1396
1647
|
}
|
|
1397
1648
|
// ── the combined runner (STRUCT-returning; INLINE sequential; native ports in; concrete row out) ──
|
|
1398
|
-
/** resolveRefAt for the top-level runner: a ref head is a PRIOR NODE (its typed scalar field) or an
|
|
1399
|
-
* INPUT PORT (in_.<field>). A REQUIRED scalar resolves to an OWNED native Rust expression; opt / non-
|
|
1400
|
-
* scalar / refOpt → null (fall back to the Value path). */
|
|
1401
|
-
function runnerResolveRef(priorNodeAt, atPos, typedNodes, plan, inputPorts) {
|
|
1402
|
-
return (head, restPath, opt) => {
|
|
1403
|
-
if (opt)
|
|
1404
|
-
return null;
|
|
1405
|
-
if (priorNodeAt(head, atPos)) {
|
|
1406
|
-
const baseRef = typedNodes.get(head);
|
|
1407
|
-
let acc;
|
|
1408
|
-
let expr;
|
|
1409
|
-
try {
|
|
1410
|
-
const r = typedFieldAccess(`${typedCell(head)}.borrow()`, baseRef, restPath, plan);
|
|
1411
|
-
expr = r.expr;
|
|
1412
|
-
acc = r.ref;
|
|
1413
|
-
}
|
|
1414
|
-
catch {
|
|
1415
|
-
return null;
|
|
1416
|
-
}
|
|
1417
|
-
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1418
|
-
return null;
|
|
1419
|
-
// OWNED: a string field is cloned; int/float/bool are Copy (deref off the Ref).
|
|
1420
|
-
const owned = acc.scalar === "string" ? `(${expr}).clone()` : `*(${expr})`;
|
|
1421
|
-
return { expr: owned, scalar: rustScalar(acc.scalar) };
|
|
1422
|
-
}
|
|
1423
|
-
if (restPath.length !== 0)
|
|
1424
|
-
return null;
|
|
1425
|
-
const scalar = inputScalarKind(inputPorts?.[head]);
|
|
1426
|
-
if (scalar === undefined)
|
|
1427
|
-
return null;
|
|
1428
|
-
const field = `in_.${rustFieldName(head)}`;
|
|
1429
|
-
const owned = scalar === "String" ? `${field}.clone()` : field;
|
|
1430
|
-
return { expr: owned, scalar };
|
|
1431
|
-
};
|
|
1432
|
-
}
|
|
1433
1649
|
function rustScalar(s) {
|
|
1434
1650
|
return s === "string" ? "String" : s === "int" ? "i64" : s === "float" ? "f64" : "bool";
|
|
1435
1651
|
}
|
|
@@ -1621,12 +1837,9 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1621
1837
|
indent += " ";
|
|
1622
1838
|
}
|
|
1623
1839
|
}
|
|
1624
|
-
const resolveRef = runnerResolveRef(priorNodeAt, k, typedNodes, plan, comp.inputPorts);
|
|
1625
1840
|
const inits = [];
|
|
1626
1841
|
for (const pn of portNames) {
|
|
1627
|
-
|
|
1628
|
-
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
|
|
1629
|
-
inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan, typedNodes));
|
|
1842
|
+
inits.push(emitPortInit(pn, node.ports[pn], comp, plan, typedNodes, (h) => priorNodeAt(h, k)));
|
|
1630
1843
|
}
|
|
1631
1844
|
const boundArg = node.bindField !== undefined ? `bound_${s}` : "None";
|
|
1632
1845
|
// bc#97: derived per-node await — this point-read's future is awaited HERE (its result-consumption
|
|
@@ -1634,22 +1847,57 @@ function emitNativeRawRunner(comp, plan, ap) {
|
|
|
1634
1847
|
// directly (no `.await`). The runner being `async fn` does not force every call site to await.
|
|
1635
1848
|
const awaitSfx = ap.nodeIsAsync(node.id) ? ".await" : "";
|
|
1636
1849
|
lines.push(`${indent}let ports_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
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;
|
|
1641
1857
|
if (meta.policy === "continue") {
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
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
|
+
}
|
|
1646
1882
|
}
|
|
1647
1883
|
else {
|
|
1648
|
-
const
|
|
1649
|
-
|
|
1650
|
-
lines.push(`${indent}
|
|
1651
|
-
lines.push(`${indent}
|
|
1652
|
-
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
|
+
}
|
|
1653
1901
|
lines.push(`${indent}produced_${s}.set(true);`);
|
|
1654
1902
|
}
|
|
1655
1903
|
for (const c of closers)
|
|
@@ -1754,12 +2002,9 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1754
2002
|
ind += " ";
|
|
1755
2003
|
}
|
|
1756
2004
|
}
|
|
1757
|
-
const resolveRef = runnerResolveRef(priorNodeAt, atPos, typedNodes, plan, comp.inputPorts);
|
|
1758
2005
|
const inits = [];
|
|
1759
2006
|
for (const pn of Object.keys(node.ports)) {
|
|
1760
|
-
|
|
1761
|
-
const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
|
|
1762
|
-
inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan, typedNodes));
|
|
2007
|
+
inits.push(emitPortInit(pn, node.ports[pn], comp, plan, typedNodes, (h) => priorNodeAt(h, atPos)));
|
|
1763
2008
|
}
|
|
1764
2009
|
if (gated) {
|
|
1765
2010
|
lines.push(`${ind}ports_${s} = Some(${structName} { ${inits.join(", ")} });`);
|
|
@@ -1777,10 +2022,26 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1777
2022
|
const nMembers = refMembers.length;
|
|
1778
2023
|
const boundN = Math.min(concurrency, nMembers);
|
|
1779
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
|
+
};
|
|
1780
2038
|
for (const i of refMembers) {
|
|
1781
2039
|
const s = sanitize(comp.body[i].id);
|
|
1782
|
-
|
|
1783
|
-
|
|
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);`);
|
|
1784
2045
|
}
|
|
1785
2046
|
lines.push(` let mut ${jobsVar}: Vec<usize> = Vec::new();`);
|
|
1786
2047
|
refMembers.forEach((i, slot) => {
|
|
@@ -1818,22 +2079,52 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
|
|
|
1818
2079
|
const s = sanitize(node.id);
|
|
1819
2080
|
const ref = typedNodes.get(node.id);
|
|
1820
2081
|
lines.push(` if ports_${s}.is_some() {`);
|
|
1821
|
-
lines.push(` let
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
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;
|
|
1825
2086
|
if (meta.policy === "continue") {
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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
|
+
}
|
|
1830
2109
|
}
|
|
1831
2110
|
else {
|
|
1832
|
-
const
|
|
1833
|
-
|
|
1834
|
-
lines.push(`
|
|
1835
|
-
lines.push(`
|
|
1836
|
-
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
|
+
}
|
|
1837
2128
|
lines.push(` produced_${s}.set(true);`);
|
|
1838
2129
|
}
|
|
1839
2130
|
lines.push(` }`);
|
|
@@ -1870,62 +2161,18 @@ isAsync = false) {
|
|
|
1870
2161
|
const portNames = Object.keys(m.ports);
|
|
1871
2162
|
const elemArrRef = typedNodes.get(node.id);
|
|
1872
2163
|
const elemName = renderTypeRef(elemArrRef.elem);
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
if (head === asName) {
|
|
1879
|
-
baseExpr = `oel_${s}`;
|
|
1880
|
-
baseRef = overElemRef;
|
|
1881
|
-
}
|
|
1882
|
-
else if (priorHere(head)) {
|
|
1883
|
-
baseExpr = `${typedCell(head)}.borrow()`;
|
|
1884
|
-
baseRef = typedNodes.get(head);
|
|
1885
|
-
}
|
|
1886
|
-
else {
|
|
1887
|
-
if (restPath.length !== 0)
|
|
1888
|
-
return null;
|
|
1889
|
-
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
1890
|
-
if (scalar === undefined)
|
|
1891
|
-
return null;
|
|
1892
|
-
const field = `in_.${rustFieldName(head)}`;
|
|
1893
|
-
return { expr: scalar === "String" ? `${field}.clone()` : field, scalar };
|
|
1894
|
-
}
|
|
1895
|
-
let acc;
|
|
1896
|
-
let expr;
|
|
1897
|
-
try {
|
|
1898
|
-
const r = typedFieldAccess(baseExpr, baseRef, restPath, plan);
|
|
1899
|
-
expr = r.expr;
|
|
1900
|
-
acc = r.ref;
|
|
1901
|
-
}
|
|
1902
|
-
catch {
|
|
1903
|
-
return null;
|
|
1904
|
-
}
|
|
1905
|
-
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
1906
|
-
return null;
|
|
1907
|
-
// OWNED value. When restPath is EMPTY, `expr` is a bare REFERENCE (`oel_${s}` = &Elem-scalar, or
|
|
1908
|
-
// `cell.borrow()` = Ref<scalar>) — deref with `*` (Copy) / `.clone()` via deref (String). When a
|
|
1909
|
-
// `.field` was appended, `expr` is a value PLACE (auto-deref) — read it directly (`.clone()` String).
|
|
1910
|
-
const isBareRef = restPath.length === 0;
|
|
1911
|
-
let owned;
|
|
1912
|
-
if (acc.scalar === "string") {
|
|
1913
|
-
owned = isBareRef ? `(*${expr}).clone()` : `${expr}.clone()`;
|
|
1914
|
-
}
|
|
1915
|
-
else {
|
|
1916
|
-
owned = isBareRef ? `*${expr}` : expr;
|
|
1917
|
-
}
|
|
1918
|
-
return { expr: owned, scalar: rustScalar(acc.scalar) };
|
|
1919
|
-
};
|
|
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) : "";
|
|
1920
2169
|
// build the per-element CONCRETE native ports struct. `il` = indent inside the element loop.
|
|
1921
2170
|
const buildPorts = (il) => {
|
|
1922
2171
|
const out = [];
|
|
1923
2172
|
const inits = [];
|
|
1924
2173
|
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
1925
2174
|
for (const pn of portNames) {
|
|
1926
|
-
|
|
1927
|
-
// #108-map: pass the element binding + base expr so a `$as` MAP/NAMED field port clones the typed composite.
|
|
1928
|
-
inits.push(emitPortInit(pn, m.ports[pn], ty, resolveRef, comp.inputPorts, plan, undefined, asBinding, `oel_${s}`));
|
|
2175
|
+
inits.push(emitPortInit(pn, m.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
|
|
1929
2176
|
}
|
|
1930
2177
|
out.push(`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`);
|
|
1931
2178
|
return out;
|
|
@@ -1935,13 +2182,7 @@ isAsync = false) {
|
|
|
1935
2182
|
const emitGuard = (il) => {
|
|
1936
2183
|
if (!hasGuard)
|
|
1937
2184
|
return [];
|
|
1938
|
-
const resolveHead = (
|
|
1939
|
-
if (head === asName)
|
|
1940
|
-
return { expr: `oel_${s}.clone()`, ref: overElemRef };
|
|
1941
|
-
if (priorHere(head))
|
|
1942
|
-
return { expr: `${typedCell(head)}.borrow().clone()`, ref: typedNodes.get(head) };
|
|
1943
|
-
return rustInputHeadRef(head, comp, plan);
|
|
1944
|
-
};
|
|
2185
|
+
const resolveHead = nativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding: { name: asName, ref: overElemRef, plan }, asBase: `oel_${s}` });
|
|
1945
2186
|
const g = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compileBool(m.when);
|
|
1946
2187
|
const out = [];
|
|
1947
2188
|
for (const st of g.stmts)
|
|
@@ -1981,26 +2222,30 @@ isAsync = false) {
|
|
|
1981
2222
|
lines.push(`${indent} let want_${s} = items_${s}.len();`);
|
|
1982
2223
|
lines.push(`${indent} let bp_${s} = ${batch} { items: items_${s} };`);
|
|
1983
2224
|
lines.push(`${indent} let row_${s} = match handlers.${nodeMethodName(node.id)}(&bp_${s}, None)${awaitSfx} {`);
|
|
1984
|
-
lines.push(`${indent}
|
|
1985
|
-
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)),`);
|
|
1986
2227
|
lines.push(`${indent} };`);
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
lines.push(`${indent} }`);
|
|
1990
|
-
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} {`);
|
|
1991
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})));`);
|
|
1992
2232
|
lines.push(`${indent} }`);
|
|
1993
2233
|
if (hasInto) {
|
|
1994
|
-
// zip the aligned batch rows onto each KEPT over element (into materializer).
|
|
1995
|
-
|
|
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};`);
|
|
1996
2237
|
lines.push(`${indent} for over_el_${s} in kept_${s}.iter() {`);
|
|
1997
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})?;`);
|
|
1998
2241
|
lines.push(`${indent} built_${s}.push(${elemMat}(over_el_${s}, er_${s}));`);
|
|
1999
2242
|
lines.push(`${indent} }`);
|
|
2000
2243
|
}
|
|
2001
2244
|
else {
|
|
2002
2245
|
// no-into: collect each element DIRECTLY from the aligned per-element row (result = kept rows).
|
|
2003
|
-
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})?;`);
|
|
2004
2249
|
lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
|
|
2005
2250
|
lines.push(`${indent} built_${s}.push(el_${s});`);
|
|
2006
2251
|
lines.push(`${indent} }`);
|
|
@@ -2014,13 +2259,21 @@ isAsync = false) {
|
|
|
2014
2259
|
lines.push(l);
|
|
2015
2260
|
for (const l of buildPorts(indent + " "))
|
|
2016
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.
|
|
2017
2265
|
lines.push(`${indent} let er_${s} = match handlers.${nodeMethodName(node.id)}(&ep_${s}, None)${awaitSfx} {`);
|
|
2018
|
-
lines.push(`${indent}
|
|
2019
|
-
|
|
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
|
+
}
|
|
2020
2273
|
lines.push(`${indent} };`);
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
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})?;`);
|
|
2024
2277
|
if (hasInto) {
|
|
2025
2278
|
lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s}, er_${s}));`);
|
|
2026
2279
|
}
|
|
@@ -2064,54 +2317,11 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
|
|
|
2064
2317
|
if (!dkField || dkField.type.kind !== "scalar" || dkField.type.scalar !== "string")
|
|
2065
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}'.`);
|
|
2066
2319
|
const dkRust = rustFieldName(f.dedupeKey);
|
|
2067
|
-
const resolveRef = (head, restPath, opt) => {
|
|
2068
|
-
if (opt)
|
|
2069
|
-
return null;
|
|
2070
|
-
let baseExpr;
|
|
2071
|
-
let baseRef;
|
|
2072
|
-
if (head === asName) {
|
|
2073
|
-
baseExpr = `oel_${s}`;
|
|
2074
|
-
baseRef = overElemRef;
|
|
2075
|
-
}
|
|
2076
|
-
else if (priorHere(head)) {
|
|
2077
|
-
baseExpr = `${typedCell(head)}.borrow()`;
|
|
2078
|
-
baseRef = typedNodes.get(head);
|
|
2079
|
-
}
|
|
2080
|
-
else {
|
|
2081
|
-
if (restPath.length !== 0)
|
|
2082
|
-
return null;
|
|
2083
|
-
const scalar = inputScalarKind(comp.inputPorts?.[head]);
|
|
2084
|
-
if (scalar === undefined)
|
|
2085
|
-
return null;
|
|
2086
|
-
const field = `in_.${rustFieldName(head)}`;
|
|
2087
|
-
return { expr: scalar === "String" ? `${field}.clone()` : field, scalar };
|
|
2088
|
-
}
|
|
2089
|
-
let acc;
|
|
2090
|
-
let expr;
|
|
2091
|
-
try {
|
|
2092
|
-
const r = typedFieldAccess(baseExpr, baseRef, restPath, plan);
|
|
2093
|
-
expr = r.expr;
|
|
2094
|
-
acc = r.ref;
|
|
2095
|
-
}
|
|
2096
|
-
catch {
|
|
2097
|
-
return null;
|
|
2098
|
-
}
|
|
2099
|
-
if (acc.kind !== "scalar" || acc.scalar === "null")
|
|
2100
|
-
return null;
|
|
2101
|
-
const isBareRef = restPath.length === 0;
|
|
2102
|
-
let owned;
|
|
2103
|
-
if (acc.scalar === "string")
|
|
2104
|
-
owned = isBareRef ? `(*${expr}).clone()` : `${expr}.clone()`;
|
|
2105
|
-
else
|
|
2106
|
-
owned = isBareRef ? `*${expr}` : expr;
|
|
2107
|
-
return { expr: owned, scalar: rustScalar(acc.scalar) };
|
|
2108
|
-
};
|
|
2109
2320
|
const buildPorts = (il) => {
|
|
2110
2321
|
const inits = [];
|
|
2111
2322
|
const asBinding = { name: asName, ref: overElemRef, plan };
|
|
2112
2323
|
for (const pn of portNames) {
|
|
2113
|
-
|
|
2114
|
-
inits.push(emitPortInit(pn, f.ports[pn], ty, resolveRef, comp.inputPorts, plan));
|
|
2324
|
+
inits.push(emitPortInit(pn, f.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
|
|
2115
2325
|
}
|
|
2116
2326
|
return [`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`];
|
|
2117
2327
|
};
|
|
@@ -2136,20 +2346,26 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
|
|
|
2136
2346
|
lines.push(`${indent} let want_${s} = pi_${s}.len();`);
|
|
2137
2347
|
lines.push(`${indent} let bp_${s} = ${batch} { items: pi_${s} };`);
|
|
2138
2348
|
lines.push(`${indent} let row_${s} = match handlers.${nodeMethodName(node.id)}(&bp_${s}, None)${awaitSfx} {`);
|
|
2139
|
-
lines.push(`${indent}
|
|
2140
|
-
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)),`);
|
|
2141
2351
|
lines.push(`${indent} };`);
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
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} {`);
|
|
2146
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})));`);
|
|
2147
2359
|
lines.push(`${indent} }`);
|
|
2148
2360
|
// ── THE ONE dedupe/drop (mirrors fanoutDedupDrop verbatim): first-seen dedupe by dedupeKey field;
|
|
2149
2361
|
// drop dangling (empty dedupeKey ≡ interpreter's null/absent-key body); implicitSource strip is
|
|
2150
2362
|
// structural (the elem type already omits it). cursor is always None (connection wrap).
|
|
2151
2363
|
lines.push(`${indent} let mut seen_${s}: std::collections::HashSet<String> = std::collections::HashSet::new();`);
|
|
2152
|
-
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} };`);
|
|
2153
2369
|
lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemRef, plan)}`);
|
|
2154
2370
|
lines.push(`${indent} let key_${s} = el_${s}.${dkRust}.clone();`);
|
|
2155
2371
|
if (f.drop === "dangling") {
|
|
@@ -2166,30 +2382,6 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
|
|
|
2166
2382
|
lines.push(c);
|
|
2167
2383
|
return lines.join("\n");
|
|
2168
2384
|
}
|
|
2169
|
-
/**
|
|
2170
|
-
* rustInputHeadRef (#108) — resolve an INPUT port head for a native-expr ref (rust). The port's declared
|
|
2171
|
-
* type comes from the shared `inputPortTypeRef` SSoT, so an OPTIONAL port resolves as `{opt:…}` (native
|
|
2172
|
-
* `Option<T>`) and the compiler can see its optionality: a null-compare becomes a real presence test and
|
|
2173
|
-
* a coalesce a real `unwrap_or`, while a REQUIRED-scalar slot fed by an opt fails closed. Exprs are OWNED
|
|
2174
|
-
* (String / Option<String> cloned; Copy scalars as-is), except an array — the only array use inside a
|
|
2175
|
-
* cond/guard expr is `len`, which BORROWS (`&in_.xs`).
|
|
2176
|
-
*/
|
|
2177
|
-
function rustInputHeadRef(head, comp, plan) {
|
|
2178
|
-
const schema = comp.inputPorts?.[head];
|
|
2179
|
-
const ref = inputPortTypeRef(schema, plan);
|
|
2180
|
-
if (ref === undefined)
|
|
2181
|
-
return null;
|
|
2182
|
-
const field = `in_.${rustFieldName(head)}`;
|
|
2183
|
-
return { expr: rustOwnedHeadExpr(field, ref), ref };
|
|
2184
|
-
}
|
|
2185
|
-
/** The OWNED native expression for a head of type `ref` read off a by-value struct field. A String (or an
|
|
2186
|
-
* Option/collection containing one) is cloned; a Copy scalar (and an Option/array of Copy scalars) is
|
|
2187
|
-
* read directly — cloning a Copy value trips clippy's `clone_on_copy`. An array stays borrowed (`len`). */
|
|
2188
|
-
function rustOwnedHeadExpr(field, ref) {
|
|
2189
|
-
if (ref.kind === "arr")
|
|
2190
|
-
return field; // borrowed by `len`
|
|
2191
|
-
return typeRefIsCopy(ref) ? field : `${field}.clone()`;
|
|
2192
|
-
}
|
|
2193
2385
|
/**
|
|
2194
2386
|
* emitCondArm — the struct-native exec of a covered cond node (#108, rust). Mirrors run_behavior's cond
|
|
2195
2387
|
* lower: compile `if` to a native bool, materialize the TAKEN branch to the node's outType (only the
|
|
@@ -2200,11 +2392,7 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
|
|
|
2200
2392
|
const c = node.cond;
|
|
2201
2393
|
const s = sanitize(node.id);
|
|
2202
2394
|
const priorHere = (head) => priorNodeAt(head, atPos);
|
|
2203
|
-
const resolveHead = (
|
|
2204
|
-
if (priorHere(head))
|
|
2205
|
-
return { expr: `${typedCell(head)}.borrow().clone()`, ref: typedNodes.get(head) };
|
|
2206
|
-
return rustInputHeadRef(head, comp, plan);
|
|
2207
|
-
};
|
|
2395
|
+
const resolveHead = nativeResolveHead(comp, plan, typedNodes, priorHere);
|
|
2208
2396
|
const compiler = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan));
|
|
2209
2397
|
const cond = compiler.compileBool(c.if);
|
|
2210
2398
|
// #125: a when(...) control-gate cond は出力データ型を持たない制御スキップマーカー。typed cell へ何も
|
|
@@ -2335,6 +2523,13 @@ pub const COMPONENT_NAMES_NATIVE_RAW: [&str; ${names.length}] = [${names.join(",
|
|
|
2335
2523
|
const UNKNOWN_COMPONENT_HELPER = `#[allow(dead_code)]
|
|
2336
2524
|
fn unknown_component(component: &str) -> BehaviorError {
|
|
2337
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)
|
|
2338
2533
|
}`;
|
|
2339
2534
|
// ── module assembly ──────────────────────────────────────────────────────────────────
|
|
2340
2535
|
function emit(ctx) {
|
|
@@ -2384,6 +2579,9 @@ ${emitStructSurface([])}
|
|
|
2384
2579
|
const handlerTraits = [];
|
|
2385
2580
|
const inStructs = [];
|
|
2386
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();
|
|
2387
2585
|
for (const c of native) {
|
|
2388
2586
|
const typedNodes = new Map();
|
|
2389
2587
|
// build the full typed-node map FIRST so a map's over-element resolution can see every node.
|
|
@@ -2401,8 +2599,11 @@ ${emitStructSurface([])}
|
|
|
2401
2599
|
else
|
|
2402
2600
|
structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
|
|
2403
2601
|
}
|
|
2602
|
+
// fail closed for a record inside a non-named top (never a silent trust) — before emitting anything.
|
|
2603
|
+
assertDeBoxableResults(c, typedNodes, plan);
|
|
2404
2604
|
rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
|
|
2405
2605
|
handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
|
|
2606
|
+
emitRustNodeDecodeFns(c, typedNodes, plan, decodeEmitted, decodeFns);
|
|
2406
2607
|
inStructs.push(emitInStruct(c, plan));
|
|
2407
2608
|
const mm = emitMapElemMaterializers(c, typedNodes, plan);
|
|
2408
2609
|
if (mm)
|
|
@@ -2445,6 +2646,12 @@ use std::cell::RefCell;`;
|
|
|
2445
2646
|
: undefined,
|
|
2446
2647
|
`// CONCRETE per-component input structs (fields = inputPorts).\n${inStructs.join("\n\n")}`,
|
|
2447
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,
|
|
2448
2655
|
elemMaterializers.length
|
|
2449
2656
|
? `// map augmented-element copiers (pure struct field assignment — no dynamic value read).\n${elemMaterializers.join("\n\n")}`
|
|
2450
2657
|
: undefined,
|
|
@@ -2476,6 +2683,160 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2476
2683
|
const anyAsyncRunner = native.some((c) => asyncPlans.get(c.name).runnerIsAsync);
|
|
2477
2684
|
// ── per-component input decoders: generic &[(String,Value)] -> concrete InNR_<comp> (test glue). ──
|
|
2478
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
|
+
: "";
|
|
2479
2840
|
// ── concrete scripted handler: implements every HandlerNR<comp> trait by decoding a scripted Value
|
|
2480
2841
|
// into the concrete per-node row (the generic scripted-source queue meets the concrete seam). ──
|
|
2481
2842
|
const decodeFns = [];
|
|
@@ -2499,22 +2860,39 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2499
2860
|
// Option<K> (NOT a boxed Value) — computed per node so the impl signature matches the trait.
|
|
2500
2861
|
const bt = boundRustType(n, typedNodes, plan);
|
|
2501
2862
|
if ("fanout" in n) {
|
|
2502
|
-
// v3: scripted fanout impl — drain one batched outcome, decode the aligned array into per-body
|
|
2503
|
-
// elem rows (a null array element decodes to the zero elem row = dangling → dropped by the arm).
|
|
2504
2863
|
const f = n.fanout;
|
|
2505
|
-
const
|
|
2864
|
+
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
2506
2865
|
const elemT = rawElemStructName(c.name, n.id);
|
|
2507
2866
|
const batchT = rawRowStructName(c.name, n.id);
|
|
2508
|
-
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2509
2867
|
const portsT = `${portsStructName(c.name, n.id)}Batch`;
|
|
2510
|
-
|
|
2511
|
-
|
|
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)}))?;
|
|
2512
2873
|
if is_err {
|
|
2513
|
-
return
|
|
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)}))?;
|
|
2892
|
+
if is_err {
|
|
2893
|
+
return Err(leaf_failure(err));
|
|
2514
2894
|
}
|
|
2515
2895
|
let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
|
|
2516
|
-
// v3: a null aligned body is a DANGLING id — decode it to the zero elem row (whose dedupeKey
|
|
2517
|
-
// field is empty), so the runner's dangling-drop excludes it (byte-equal to fanoutDedupDrop).
|
|
2518
2896
|
let rows = arr
|
|
2519
2897
|
.iter()
|
|
2520
2898
|
.map(|ev| match ev {
|
|
@@ -2522,7 +2900,7 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2522
2900
|
_ => ${elemDecode}(ev),
|
|
2523
2901
|
})
|
|
2524
2902
|
.collect();
|
|
2525
|
-
|
|
2903
|
+
Ok(${batchT} { rows })
|
|
2526
2904
|
}`);
|
|
2527
2905
|
continue;
|
|
2528
2906
|
}
|
|
@@ -2530,65 +2908,172 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
|
|
|
2530
2908
|
const m = n.map;
|
|
2531
2909
|
const batched = m.batched === true;
|
|
2532
2910
|
const arrRef = ref;
|
|
2533
|
-
const
|
|
2911
|
+
const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
|
|
2534
2912
|
const elemT = rawElemStructName(c.name, n.id);
|
|
2535
2913
|
const batchT = rawRowStructName(c.name, n.id);
|
|
2536
|
-
const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
|
|
2537
2914
|
const portsT = batched ? `${portsStructName(c.name, n.id)}Batch` : portsStructName(c.name, n.id);
|
|
2538
|
-
|
|
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");
|
|
2539
2941
|
if (batched) {
|
|
2540
|
-
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) ->
|
|
2541
|
-
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)}))?;
|
|
2542
2944
|
if is_err {
|
|
2543
|
-
return
|
|
2945
|
+
return Err(leaf_failure(err));
|
|
2544
2946
|
}
|
|
2545
2947
|
let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
|
|
2546
2948
|
let rows = arr.iter().map(${elemDecode}).collect();
|
|
2547
|
-
|
|
2949
|
+
Ok(${batchT} { rows })
|
|
2548
2950
|
}`);
|
|
2549
2951
|
}
|
|
2550
2952
|
else {
|
|
2551
|
-
methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) ->
|
|
2552
|
-
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)}))?;
|
|
2553
2955
|
if is_err {
|
|
2554
|
-
return
|
|
2956
|
+
return Err(leaf_failure(err));
|
|
2555
2957
|
}
|
|
2556
2958
|
let v = ok.unwrap_or(Value::Null);
|
|
2557
|
-
|
|
2959
|
+
Ok(${elemDecode}(&v))
|
|
2558
2960
|
}`);
|
|
2559
2961
|
}
|
|
2560
|
-
void retT;
|
|
2561
2962
|
continue;
|
|
2562
2963
|
}
|
|
2563
2964
|
const node = n;
|
|
2564
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
|
+
}
|
|
2565
3047
|
const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
|
|
2566
|
-
// #129 (TEST glue):
|
|
2567
|
-
//
|
|
2568
|
-
//
|
|
2569
|
-
//
|
|
2570
|
-
//
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
.map((p) => `\n if echo == ${rustStrLit(p)} {\n return Some(${rowT} { val: ${echoRead(p)}, ..Default::default() });\n }`)
|
|
2580
|
-
.join("");
|
|
2581
|
-
const okBind = echoable.length > 0 ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
|
|
2582
|
-
methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
|
|
2583
|
-
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)}))?;
|
|
2584
3061
|
if is_err {
|
|
2585
|
-
return
|
|
3062
|
+
return Err(leaf_failure(err));
|
|
2586
3063
|
}${echoBranch}
|
|
2587
3064
|
let v = ok.unwrap_or(Value::Null);
|
|
2588
|
-
|
|
3065
|
+
Ok(${decode}(&v))
|
|
2589
3066
|
}`);
|
|
2590
3067
|
}
|
|
2591
|
-
|
|
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}`);
|
|
2592
3077
|
}
|
|
2593
3078
|
const arms = native
|
|
2594
3079
|
.map((c) => {
|
|
@@ -2637,7 +3122,7 @@ impl ScriptedNativeRaw {
|
|
|
2637
3122
|
}
|
|
2638
3123
|
|
|
2639
3124
|
// drain one scripted outcome for a component (last entry repeats — mirrors the reference scripted
|
|
2640
|
-
// handlers); None when the component has no scripted queue
|
|
3125
|
+
// handlers); None when the component has no scripted queue. Returns (is_error, err,
|
|
2641
3126
|
// ok_value, echo). The bc#87 parallel-stage runner calls node_* from multiple threads, so the drain
|
|
2642
3127
|
// is Mutex-guarded. #129: \`echo\` carries the reference scriptedHandlers' \`echo\` directive (e.g.
|
|
2643
3128
|
// "items") so a scripted node can ECHO a named input PORT back — used to make the node→leaf array
|
|
@@ -2647,13 +3132,11 @@ impl ScriptedNativeRaw {
|
|
|
2647
3132
|
let mut queues = self.queues.lock().unwrap();
|
|
2648
3133
|
let (queue, single) = queues.get_mut(component)?;
|
|
2649
3134
|
let raw = if *single || queue.len() <= 1 { queue[0].clone() } else { queue.remove(0) };
|
|
2650
|
-
//
|
|
2651
|
-
//
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
Some(other) => other.to_string(),
|
|
2656
|
-
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() }
|
|
2657
3140
|
};
|
|
2658
3141
|
if let Some(okv) = raw.get("ok") {
|
|
2659
3142
|
let v = behavior_contracts::decode_value(okv).expect("decode ok");
|
|
@@ -2681,9 +3164,27 @@ use behavior_contracts::Value;
|
|
|
2681
3164
|
use super::*;
|
|
2682
3165
|
|
|
2683
3166
|
// bridge the covered module's LOCAL BehaviorError (super::BehaviorError) to the bc-runtime failure type
|
|
2684
|
-
// 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.
|
|
2685
3169
|
fn to_rt_err(e: BehaviorError) -> behavior_contracts::BehaviorError {
|
|
2686
|
-
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
|
|
2687
3188
|
}
|
|
2688
3189
|
${anyAsyncRunner
|
|
2689
3190
|
? `// bc#97 (TEST glue) — a minimal std-only block-on that drives an async runner Future to completion so
|
|
@@ -2723,6 +3224,9 @@ ${decodeFns.join("\n\n")}
|
|
|
2723
3224
|
// input decoders (generic &[(String,Value)] -> concrete InNR_<comp>; TEST glue, one decode at the boundary).
|
|
2724
3225
|
${inDecoders}
|
|
2725
3226
|
|
|
3227
|
+
// scripted wire adapter (TEST glue) — wraps a scripted Value as a WireRow the generated de-box probes.
|
|
3228
|
+
${wireAdapter}
|
|
3229
|
+
|
|
2726
3230
|
${adapter}
|
|
2727
3231
|
|
|
2728
3232
|
// scripted handler trait impls (one per covered component — the concrete node_* seam, test glue).
|
|
@@ -2745,10 +3249,8 @@ ${arms}
|
|
|
2745
3249
|
}
|
|
2746
3250
|
/** emitInDecoder — a TEST-glue decoder `decode_InNR_<comp>(input) -> Result<InNR_<comp>, BehaviorError>`.
|
|
2747
3251
|
* Reads each declared input port from the generic &[(String,Value)] into the concrete field (scalar →
|
|
2748
|
-
* typed read;
|
|
2749
|
-
*
|
|
2750
|
-
* same value `refCore` observes for the `null` binding run_behavior is given. Lives ONLY in the observe
|
|
2751
|
-
* 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. */
|
|
2752
3254
|
function emitInDecoder(comp, plan) {
|
|
2753
3255
|
const name = inStructName(comp.name);
|
|
2754
3256
|
const ports = Object.keys(comp.inputPorts ?? {});
|
|
@@ -2770,15 +3272,7 @@ function emitInDecoder(comp, plan) {
|
|
|
2770
3272
|
const et = schema.elemType;
|
|
2771
3273
|
const head = i === 0 ? " if" : " } else if";
|
|
2772
3274
|
lines.push(`${head} k == ${rustStrLit(p)} {`);
|
|
2773
|
-
if (
|
|
2774
|
-
// An OPTIONAL port decodes into its native Option<T>. materializeExpr's opt de-box IS the wire
|
|
2775
|
-
// rule: `Value::Null` → None (absent), any other value → Some(<inner>). A key that never appears
|
|
2776
|
-
// keeps the struct Default (None) — the same absent value. Checked FIRST: optionality wraps every
|
|
2777
|
-
// inner type (an optional array is Option<Vec<T>>, not Vec<T>).
|
|
2778
|
-
const optRef = inputPortTypeRef(schema, plan);
|
|
2779
|
-
lines.push(` in_.${field} = ${materializeExpr("v", optRef, plan)};`);
|
|
2780
|
-
}
|
|
2781
|
-
else if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
3275
|
+
if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
|
|
2782
3276
|
// #108: decode the boxed Value::Arr into the native Vec<ElemT> (test glue — materialize each elem).
|
|
2783
3277
|
const elemRef = deriveTypeRef(et, plan);
|
|
2784
3278
|
const elemTy = renderTypeRef(elemRef);
|
|
@@ -2798,6 +3292,11 @@ function emitInDecoder(comp, plan) {
|
|
|
2798
3292
|
const mapRef = { kind: "map", value: deriveTypeRef(et, plan) };
|
|
2799
3293
|
lines.push(` in_.${field} = ${materializeExpr("v", mapRef, plan)};`);
|
|
2800
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
|
+
}
|
|
2801
3300
|
else if (scalar === undefined) {
|
|
2802
3301
|
lines.push(` in_.${field} = v.clone();`);
|
|
2803
3302
|
}
|
|
@@ -2816,16 +3315,6 @@ function emitInDecoder(comp, plan) {
|
|
|
2816
3315
|
lines.push(`}`);
|
|
2817
3316
|
return lines.join("\n");
|
|
2818
3317
|
}
|
|
2819
|
-
/** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
|
|
2820
|
-
* declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row's `val`. A
|
|
2821
|
-
* port with a different type is not echoable (the row could not hold it), and a NAMED outType has no
|
|
2822
|
-
* `val` at all (its row carries the struct's fields flattened — see emitDecodeRow). */
|
|
2823
|
-
function echoablePorts(comp, node, ref, plan, typedNodes) {
|
|
2824
|
-
if (ref.kind === "named")
|
|
2825
|
-
return [];
|
|
2826
|
-
const want = renderTypeRef(ref);
|
|
2827
|
-
return Object.keys(node.ports).filter((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}'`, undefined, plan, typedNodes) === want);
|
|
2828
|
-
}
|
|
2829
3318
|
/** emitDecodeRow — emit (once, deduped) a decoder `decode_row_<comp>_<node><suffix>(v: &Value) -> <rowT>`
|
|
2830
3319
|
* that materializes the scripted ok Value into the concrete row struct. Named: reuse the Value->struct
|
|
2831
3320
|
* marshaller + move fields; scalar/arr/opt: materialize into .val. TEST glue only. */
|
|
@@ -2840,12 +3329,12 @@ function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix)
|
|
|
2840
3329
|
const decl = findDecl(plan, ref.name);
|
|
2841
3330
|
lines.push(` let t = marshal_${ref.name}(v).expect("scripted row decode");`);
|
|
2842
3331
|
const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: t.${rustFieldName(f.name)}`).join(", ");
|
|
2843
|
-
lines.push(` ${rowT} { ${inits}
|
|
3332
|
+
lines.push(` ${rowT} { ${inits} }`);
|
|
2844
3333
|
}
|
|
2845
3334
|
else {
|
|
2846
3335
|
const mat = materializeExpr("v", ref, plan);
|
|
2847
3336
|
lines.push(` let val = (|| -> Result<${renderTypeRef(ref)}, BehaviorError> { Ok(${mat}) })().expect("scripted row decode");`);
|
|
2848
|
-
lines.push(` ${rowT} { val
|
|
3337
|
+
lines.push(` ${rowT} { val }`);
|
|
2849
3338
|
}
|
|
2850
3339
|
lines.push(`}`);
|
|
2851
3340
|
out.push(lines.join("\n"));
|