relon_eval_api/inplace_return.rs
1//! Backend-shared host side of the in-place region-walk return ABI
2//! (S1/S2 `List<List<scalar>>`, S3 `List<String>`, S4 `List<Schema>`,
3//! and pointer-array variant lists such as `List<Enum>`).
4//! When a compiled
5//! `#main` returns a parameter-sourced pointer-array list, the machine
6//! code does **not** copy the value into `out_buf`. Instead the epilogue
7//! reports the arena-absolute offset of the return root via the negative
8//! sentinel `-(root_abs + 1)`. The host then:
9//!
10//! 1. recovers `root_abs` from the sentinel ([`decode_inplace_sentinel`]),
11//! 2. selects the arena region `root_abs` lands in,
12//! 3. runs the bounds [`verify_value_at`] over the whole reachable graph
13//! confined to that region — **a verify failure aborts the decode**,
14//! 4. only on a clean verify decodes the value in place via the matching
15//! positional reader (`read_list_list_at` / `read_list_string_at` /
16//! `read_list_record_at`).
17//!
18//! Both the cranelift and llvm backends call into this one
19//! implementation, so the sentinel → region-select → verifier → decode
20//! pipeline is genuinely backend-shared rather than mirrored per crate.
21//! The machine-code side (which negative sentinel to emit) is the only
22//! per-backend half; the host decode is here.
23
24use std::sync::Arc;
25
26use crate::buffer::BufferReader;
27use crate::layout::{OffsetTable, SchemaLayout};
28use crate::schema_canonical::{Field, Schema, TypeRepr};
29use crate::smol_str::SmolStr;
30use crate::value::Value;
31use crate::verifier::{verify_record_multi, verify_value_at_multi, MultiRegion, VerifyError};
32use crate::RuntimeError;
33
34/// Arena region boundaries the in-place decode selects between. The
35/// arena layout (shared by both AOT backends) is
36/// `[const_data | pad | in_buf | pad | out_buf | pad | scratch]`; the
37/// returned root may live in any region (S1 only ever sees the
38/// param-sourced `in_buf` list, but the selection is generic so S2+ can
39/// return `out_buf` / `scratch` roots through the same gate).
40#[derive(Debug, Clone, Copy)]
41pub struct ArenaRegions {
42 /// Length of the const-data section at arena offset 0.
43 pub const_data_len: usize,
44 /// Input region start (`in_ptr`) and length.
45 pub in_ptr: u32,
46 pub in_len: u32,
47 /// Output region start (`out_ptr`) and capacity.
48 pub out_ptr: u32,
49 pub out_cap: u32,
50 /// Scratch region start; it runs to `arena_size`.
51 pub scratch_base: u32,
52 /// Total arena size in bytes.
53 pub arena_size: usize,
54}
55
56impl ArenaRegions {
57 /// Build the four-region [`MultiRegion`] map in absolute arena
58 /// coordinates from the dispatch's region boundaries. Every slot
59 /// pointer the F1 ABI emits is arena-absolute, so the verifier walks
60 /// the **whole arena** and classifies each followed span into one of
61 /// `const` / `in` / `out` / `scratch`. The ABI lays the regions out
62 /// disjointly as `[const | pad | in | pad | out | pad | scratch]`; we
63 /// pass each as a half-open `[start, end)` window.
64 pub fn multi_region(&self) -> Result<MultiRegion, VerifyError> {
65 let in_start = self.in_ptr as usize;
66 let in_end = in_start + self.in_len as usize;
67 let out_start = self.out_ptr as usize;
68 let out_end = out_start + self.out_cap as usize;
69 let scratch_start = self.scratch_base as usize;
70 MultiRegion::new(
71 (0, self.const_data_len),
72 (in_start, in_end),
73 (out_start, out_end),
74 (scratch_start, self.arena_size),
75 )
76 }
77}
78
79/// Decode the in-place region-walk return sentinel. The machine code
80/// encodes an in-place return as `-(root_abs + 1)` (a value `<= -9`,
81/// since `root_abs >= in_ptr >= 8`). Recover `root_abs = -ret - 1`,
82/// rejecting a sentinel that doesn't round-trip into a non-negative
83/// offset (a corrupt / impossible encoding).
84///
85/// `ret` must already be known negative (the caller distinguishes the
86/// non-negative `bytes_written` path before calling).
87pub fn decode_inplace_sentinel(ret: i32) -> Result<usize, RuntimeError> {
88 // `ret` is negative here. `-(ret as i64) - 1` is the root offset;
89 // i64 math avoids the `i32::MIN` negation overflow.
90 let root = -(ret as i64) - 1;
91 if root < 0 {
92 return Err(RuntimeError::IoError(format!(
93 "in-place return sentinel {ret} decodes to a negative root offset"
94 )));
95 }
96 usize::try_from(root).map_err(|_| {
97 RuntimeError::IoError(format!(
98 "in-place return sentinel {ret} decodes to an out-of-range root offset"
99 ))
100 })
101}
102
103/// Decode an in-place region-walk return for a single-value return whose
104/// root is a parameter-sourced pointer-array list: `List<List<scalar>>`
105/// (S1/S2), `List<String>` (S3), `List<Schema>` (S4), or a variant list
106/// such as `List<Option<T>>` / `List<Result<T, E>>` / `List<Enum>`. The machine
107/// code reported `root_abs`
108/// (the arena-absolute offset of the return root) via the negative
109/// sentinel; the caller already recovered it with
110/// [`decode_inplace_sentinel`].
111///
112/// `return_field` is the single return-schema field; `return_layout` /
113/// `return_fields` are the matching return [`OffsetTable`] and fields the
114/// [`BufferReader`] decodes against. `backend` is a short label
115/// (`"cranelift"` / `"llvm"`) for diagnostics.
116///
117/// This is backend-agnostic: it owns the region-select + verifier +
118/// decode pipeline so both AOT backends share exactly one host
119/// implementation. It dispatches on the return field's declared type so a
120/// single sentinel path covers every in-place shape; an unexpected type
121/// reaching here is a lowering/ABI drift bug surfaced loudly.
122pub fn decode_inplace_return(
123 backend: &str,
124 arena: &[u8],
125 regions: ArenaRegions,
126 root_abs: usize,
127 return_field: &Field,
128 return_layout: &OffsetTable,
129 return_fields: &[Field],
130) -> Result<Value, RuntimeError> {
131 // The return must be a pointer-array list the in-place ABI emits:
132 // `List<List<scalar>>`, `List<String>`, `List<Schema>`, or a variant list. Classify it
133 // up front so the region/verify pipeline below is shape-agnostic and
134 // only the final decode branches. Anything else is ABI drift — surface
135 // it loudly.
136 // The `List` prefix on every variant is intentional: each names the
137 // concrete pointer-array return shape (`List<List>` / `List<String>` /
138 // `List<Schema>` / `List<Variant>`) the sentinel can carry, so the shared prefix is the
139 // point rather than noise.
140 #[allow(clippy::enum_variant_names)]
141 enum InplaceShape<'a> {
142 /// `List<List<scalar>>`; carries the innermost scalar element type.
143 ListListScalar(TypeRepr),
144 /// `List<String>`.
145 ListString,
146 /// `List<Schema>`; carries the per-element sub-record schema.
147 ListSchema(&'a Schema),
148 /// F5: a doubly-nested pointer-array list — `List<List<String>>`
149 /// / `List<List<Schema>>` (and deeper). Carries the **outer list
150 /// element** type (`List<String>` / `List<Schema>`); the unified
151 /// recursive reader walks one level deeper than the scalar path.
152 ListListPointerArray(&'a TypeRepr),
153 /// A pointer-array list of variant records: `List<Option<T>>`,
154 /// `List<Result<T, E>>`, or `List<CustomEnum>`.
155 ListVariant(&'a TypeRepr),
156 }
157 let shape = match &return_field.ty {
158 TypeRepr::List { element } => match element.as_ref() {
159 TypeRepr::List { element: inner } => match inner.as_ref() {
160 // Inner inline-fixed scalar: the S1/S2 path.
161 TypeRepr::Int | TypeRepr::Float | TypeRepr::Bool => {
162 InplaceShape::ListListScalar(inner.as_ref().clone())
163 }
164 // Inner pointer-array element (String / Schema / deeper
165 // List): the F5 recursive path. `element` is the outer
166 // list element (`List<String>` / `List<Schema>`).
167 _ => InplaceShape::ListListPointerArray(element.as_ref()),
168 },
169 TypeRepr::String => InplaceShape::ListString,
170 TypeRepr::Schema { schema } => InplaceShape::ListSchema(schema.as_ref()),
171 TypeRepr::Option { .. } | TypeRepr::Result { .. } | TypeRepr::Enum { .. } => {
172 InplaceShape::ListVariant(element.as_ref())
173 }
174 other => {
175 return Err(RuntimeError::IoError(format!(
176 "{backend} in-place return: unsupported list element {other:?} \
177 (expected List<scalar>, String, Schema, Option, Result, or Enum)"
178 )));
179 }
180 },
181 other => {
182 return Err(RuntimeError::IoError(format!(
183 "{backend} in-place return: expected a pointer-array List, got {other:?}"
184 )));
185 }
186 };
187
188 // 1. Build the multi-region map over the whole arena. Under the F1
189 // arena-absolute slot convention every pointer the in-place root
190 // reaches is an arena-absolute offset, so the verifier and reader
191 // both walk the **whole arena** rather than a region slice; the
192 // multi-region map classifies each followed span into the one region
193 // it belongs to (param-sourced data in `in`, copied tails in `out`,
194 // const-pool literals in `const`, scratch). The single-value root
195 // still lives in exactly one region, but a cross-region link is now
196 // legal and bounds-checked region-by-region rather than rejected.
197 let arena_size = regions.arena_size;
198 if arena_size > arena.len() {
199 return Err(RuntimeError::IoError(format!(
200 "{backend} in-place return arena_size {arena_size} exceeds arena slice {}",
201 arena.len()
202 )));
203 }
204 let arena = &arena[..arena_size];
205 let multi = regions.multi_region().map_err(|e| {
206 RuntimeError::IoError(format!(
207 "{backend} in-place return arena regions invalid: {e}"
208 ))
209 })?;
210 if root_abs >= arena_size {
211 return Err(RuntimeError::IoError(format!(
212 "{backend} in-place return root {root_abs} is past arena end {arena_size}"
213 )));
214 }
215
216 // 2. Recompute the `ListElementKind` sidecar for the outer
217 // `List<List<…>>` from the return layout so the verifier knows the
218 // root is a pointer-array header.
219 let list_element = return_layout.fields.first().and_then(|fo| fo.list_element);
220
221 // 3. Verify the whole reachable graph stays inside the arena regions
222 // BEFORE any decode. A failure is loud and aborts — never a wild read.
223 verify_value_at_multi(arena, &return_field.ty, list_element, root_abs, multi).map_err(|e| {
224 RuntimeError::IoError(format!(
225 "{backend} in-place return verifier rejected the buffer (root_abs={root_abs}): {e}"
226 ))
227 })?;
228
229 // 4. Verified — decode in place. The reader walks the whole arena the
230 // verifier certified; every slot value is arena-absolute, so an
231 // in-place decode is byte-equal (post-decode) to the field-slot
232 // reader the tree-walk oracle's writer produced.
233 let reader = BufferReader::new(return_layout, return_fields, arena)
234 .map_err(|e| RuntimeError::IoError(format!("{backend} buffer: {e}")))?;
235 match shape {
236 InplaceShape::ListListScalar(inner) => {
237 let rows = reader
238 .read_list_list_at(root_abs, &inner)
239 .map_err(|e| RuntimeError::IoError(format!("{backend} buffer: {e}")))?;
240 Ok(Value::List(Arc::new(
241 rows.into_iter().map(|r| Value::List(Arc::new(r))).collect(),
242 )))
243 }
244 InplaceShape::ListString => {
245 let items = reader
246 .read_list_string_at(root_abs)
247 .map_err(|e| RuntimeError::IoError(format!("{backend} buffer: {e}")))?;
248 Ok(Value::List(Arc::new(
249 items.into_iter().map(|s| Value::String(s.into())).collect(),
250 )))
251 }
252 InplaceShape::ListListPointerArray(outer_element) => {
253 // The verifier already certified the whole graph (outer
254 // entries → inner list headers → inner entries → String /
255 // sub-record / String-field layer). Decode in place via the
256 // unified recursive reader: `root_abs` is the outer header, and
257 // `outer_element` (`List<String>` / `List<Schema>`) drives one
258 // level of recursion per outer entry.
259 let rows = reader
260 .read_list_value_at(root_abs, outer_element)
261 .map_err(|e| RuntimeError::IoError(format!("{backend} buffer: {e}")))?;
262 Ok(Value::List(Arc::new(rows)))
263 }
264 InplaceShape::ListVariant(element) => {
265 let rows = reader
266 .read_list_value_at(root_abs, element)
267 .map_err(|e| RuntimeError::IoError(format!("{backend} buffer: {e}")))?;
268 Ok(Value::List(Arc::new(rows)))
269 }
270 InplaceShape::ListSchema(schema) => {
271 // The verifier already certified the whole graph: outer
272 // entries, each sub-record's fixed area, and every String /
273 // List field pointer the sub-record carries (see
274 // `verify_pointer_target` -> `TypeRepr::Schema`). Decode each
275 // entry's sub-record into a branded dict, positionally, sharing
276 // the same region slice — bit-identical to the field-slot
277 // `read_list_record` path the tree-walk oracle's writer feeds.
278 let elem_layout = SchemaLayout::offsets_for(schema).map_err(|e| {
279 RuntimeError::IoError(format!(
280 "{backend} in-place List<Schema> element `{}` layout: {e}",
281 schema.name
282 ))
283 })?;
284 let sub_readers = reader
285 .read_list_record_at(root_abs, &elem_layout, schema)
286 .map_err(|e| RuntimeError::IoError(format!("{backend} buffer: {e}")))?;
287 let mut items = Vec::with_capacity(sub_readers.len());
288 for sub in &sub_readers {
289 if schema.is_tuple {
290 items.push(read_tuple_record_into_tuple(backend, sub, schema)?);
291 } else {
292 let map = read_record_into_branded_map(backend, sub, schema)?;
293 items.push(Value::branded_dict(map, Some(schema.name.clone())));
294 }
295 }
296 Ok(Value::List(Arc::new(items)))
297 }
298 }
299}
300
301/// Gate the **object** (positive-`bytes_written`) return path through the
302/// multi-region bounds verifier before its `BufferReader` decode runs —
303/// closing the red-line gap the S5 design flagged (an object return
304/// previously trusted `out_buf` self-containment and ran no verifier at
305/// all).
306///
307/// Under the F1 arena-absolute slot convention the object head (anchored
308/// at the arena-absolute `record_base`, i.e. `out_ptr`) and every pointer
309/// it reaches are arena-absolute offsets into the whole `arena`. The
310/// `multi` map confines each followed span to one of `const` / `in` /
311/// `out` / `scratch` and bounds-checks it there: today every object field
312/// is still self-contained in `out_buf` (cross-region object fields stay
313/// capped — F1b releases them), so every span lands in `out`, but the
314/// gate is already cross-region-correct so F1b is a cap flip, not a
315/// verifier change. A verify failure is a loud error — the decode must
316/// not run.
317pub fn verify_object_return_multi(
318 backend: &str,
319 arena: &[u8],
320 record_base: usize,
321 multi: MultiRegion,
322 return_layout: &OffsetTable,
323 return_fields: &[Field],
324) -> Result<(), RuntimeError> {
325 verify_record_multi(arena, return_layout, return_fields, record_base, multi).map_err(|e| {
326 RuntimeError::IoError(format!(
327 "{backend} cross-region object return verifier rejected the arena (base={record_base}): {e}"
328 ))
329 })
330}
331
332/// Single central entry for the **object** (positive-`bytes_written`)
333/// return path, shared by both AOT backends. It enforces the one correct
334/// order — `verify_object_return_multi` **before** any decode — so no
335/// present or future object-return caller can reach a `BufferReader`
336/// without the multi-region bounds gate having run first. A verify
337/// failure aborts loudly; the decode never runs on an unverified arena.
338///
339/// `arena` must already be sliced to `regions.arena_size` by the caller
340/// (each backend owns the read-length bounds check against its own arena
341/// allocation). `record_base` is the object head (`out_ptr`).
342/// `single_value_wrapper` is the backend's `is_single_value_wrapper`
343/// decision (it depends on `relon-ir` schema-name constants this leaf
344/// crate intentionally does not pull in); when set, the single
345/// return-schema field is decoded directly, otherwise the whole record is
346/// drained into a branded dict.
347///
348/// The decode walks the shared [`read_object_field`] / [`BufferReader`]
349/// path for both backends, so the produced [`Value`] is byte-identical
350/// regardless of which AOT backend invoked it.
351#[allow(clippy::too_many_arguments)]
352pub fn decode_object_return(
353 backend: &str,
354 arena: &[u8],
355 record_base: usize,
356 regions: ArenaRegions,
357 return_layout: &OffsetTable,
358 return_schema: &Schema,
359 single_value_wrapper: bool,
360) -> Result<Value, RuntimeError> {
361 let multi = regions.multi_region().map_err(|e| {
362 RuntimeError::IoError(format!(
363 "{backend} object return arena regions invalid: {e}"
364 ))
365 })?;
366 // Bounds gate FIRST — must precede every decode below.
367 verify_object_return_multi(
368 backend,
369 arena,
370 record_base,
371 multi,
372 return_layout,
373 &return_schema.fields,
374 )?;
375 let reader =
376 BufferReader::new_at_base(return_layout, &return_schema.fields, arena, record_base)
377 .map_err(|e| RuntimeError::IoError(format!("{backend} buffer: {e}")))?;
378 if single_value_wrapper {
379 let field = &return_schema.fields[0];
380 read_object_field(backend, &reader, field, return_schema)
381 } else if return_schema.is_tuple {
382 // A tuple schema is laid out and verified exactly like any other
383 // record, but it decodes **positionally** to a `Value::Tuple`
384 // (declaration-order slots, no field names). JSON projection still
385 // collapses it to an array.
386 read_tuple_record_into_tuple(backend, &reader, return_schema)
387 } else {
388 let map = read_object_record_into_map(backend, &reader, return_schema)?;
389 Ok(Value::branded_dict(map, Some(return_schema.name.clone())))
390 }
391}
392
393/// Drain a tuple schema's fields in declaration order into a positional
394/// `Value::Tuple`. The slot readers are the same
395/// [`read_object_field`] arms a branded record uses; only the container
396/// shape differs (ordered tuple vs named map), which is exactly the
397/// tuple-vs-record decode fork.
398fn read_tuple_record_into_tuple(
399 backend: &str,
400 reader: &BufferReader<'_>,
401 schema: &Schema,
402) -> Result<Value, RuntimeError> {
403 let mut items = Vec::with_capacity(schema.fields.len());
404 for field in &schema.fields {
405 items.push(read_object_field(backend, reader, field, schema)?);
406 }
407 Ok(Value::Tuple(Arc::new(items)))
408}
409
410/// Drain every field of `schema` from the object record `reader` into a
411/// sorted `BTreeMap<SmolStr, Value>`. Backend-shared body of the object /
412/// nested-record decode (replaces the two per-crate `read_record_into_map`
413/// copies the cranelift / llvm evaluators carried).
414fn read_object_record_into_map(
415 backend: &str,
416 reader: &BufferReader<'_>,
417 schema: &Schema,
418) -> Result<std::collections::BTreeMap<SmolStr, Value>, RuntimeError> {
419 let mut map = std::collections::BTreeMap::new();
420 for field in &schema.fields {
421 let value = read_object_field(backend, reader, field, schema)?;
422 map.insert(SmolStr::from(field.name.as_str()), value);
423 }
424 Ok(map)
425}
426
427/// Decode one object-return field via [`BufferReader`] into a [`Value`].
428/// This is the single backend-shared copy of the object-path
429/// `read_value_from_reader` the cranelift and llvm evaluators each carried
430/// (debt 1): adding a new field type now changes exactly this one arm set.
431///
432/// It covers the full object-field type space — scalars (`Int` / `Float` /
433/// `Bool` / internal unit), `String`, every `List<…>` shape, and a bare nested
434/// `Schema` (sub-record) — by mapping each leaf onto the matching
435/// [`BufferReader`] reader. The `List<List<scalar>>` arm keeps the
436/// dedicated `read_list_list` reader (inline-fixed inner rows); every
437/// pointer-array list (`List<Schema>` / `List<List<String|Schema>>`)
438/// routes through the unified recursive `read_list_value`, exactly as both
439/// backends did. `parent_schema` only feeds the unsupported-type
440/// diagnostic. The verifier in [`decode_object_return`] has already
441/// certified every pointer this reader follows.
442fn read_object_field(
443 backend: &str,
444 reader: &BufferReader<'_>,
445 field: &Field,
446 parent_schema: &Schema,
447) -> Result<Value, RuntimeError> {
448 let map_err =
449 |e: crate::buffer::BufferError| RuntimeError::IoError(format!("{backend} buffer: {e}"));
450 match &field.ty {
451 TypeRepr::Int => reader
452 .read_int(&field.name)
453 .map(Value::Int)
454 .map_err(map_err),
455 TypeRepr::Float => reader
456 .read_float(&field.name)
457 .map(|f| Value::Float(ordered_float::OrderedFloat(f)))
458 .map_err(map_err),
459 TypeRepr::Bool => reader
460 .read_bool(&field.name)
461 .map(Value::Bool)
462 .map_err(map_err),
463 TypeRepr::Unit => reader
464 .read_unit(&field.name)
465 .map(|()| Value::option_none())
466 .map_err(map_err),
467 TypeRepr::String => reader
468 .read_string(&field.name)
469 .map(|s| Value::String(s.into()))
470 .map_err(map_err),
471 // Bare nested Schema: anchor a sub-reader at the slot's
472 // sub-record fixed area and recurse into a branded dict.
473 TypeRepr::Schema { schema } => {
474 let sub_layout = SchemaLayout::offsets_for(schema).map_err(|e| {
475 RuntimeError::IoError(format!(
476 "{backend} object return field `{}` layout: {e}",
477 field.name
478 ))
479 })?;
480 let sub_reader = reader
481 .sub_record(&field.name, &sub_layout, &schema.fields)
482 .map_err(map_err)?;
483 let map = read_object_record_into_map(backend, &sub_reader, schema)?;
484 Ok(Value::branded_dict(map, Some(schema.name.clone())))
485 }
486 TypeRepr::Option { .. } | TypeRepr::Result { .. } | TypeRepr::Enum { .. } => {
487 reader.read_value(&field.name, &field.ty).map_err(map_err)
488 }
489 TypeRepr::List { element } => match element.as_ref() {
490 TypeRepr::Int => reader
491 .read_list_int(&field.name)
492 .map(|v| Value::List(Arc::new(v.into_iter().map(Value::Int).collect())))
493 .map_err(map_err),
494 TypeRepr::Float => reader
495 .read_list_float(&field.name)
496 .map(|v| {
497 Value::List(Arc::new(
498 v.into_iter()
499 .map(|f| Value::Float(ordered_float::OrderedFloat(f)))
500 .collect(),
501 ))
502 })
503 .map_err(map_err),
504 TypeRepr::Bool => reader
505 .read_list_bool(&field.name)
506 .map(|v| Value::List(Arc::new(v.into_iter().map(Value::Bool).collect())))
507 .map_err(map_err),
508 TypeRepr::String => reader
509 .read_list_string(&field.name)
510 .map(|v| {
511 Value::List(Arc::new(
512 v.into_iter().map(|s| Value::String(s.into())).collect(),
513 ))
514 })
515 .map_err(map_err),
516 TypeRepr::Schema { schema } => {
517 let elem_layout = SchemaLayout::offsets_for(schema).map_err(|e| {
518 RuntimeError::IoError(format!(
519 "{backend} object return List<Schema> element `{}` layout: {e}",
520 schema.name
521 ))
522 })?;
523 let sub_readers = reader
524 .read_list_record(&field.name, &elem_layout, schema)
525 .map_err(map_err)?;
526 let mut items = Vec::with_capacity(sub_readers.len());
527 for sub in &sub_readers {
528 if schema.is_tuple {
529 items.push(read_tuple_record_into_tuple(backend, sub, schema)?);
530 } else {
531 let map = read_object_record_into_map(backend, sub, schema)?;
532 items.push(Value::branded_dict(map, Some(schema.name.clone())));
533 }
534 }
535 Ok(Value::List(Arc::new(items)))
536 }
537 TypeRepr::Option { .. } | TypeRepr::Result { .. } | TypeRepr::Enum { .. } => reader
538 .read_list_value(&field.name, element.as_ref())
539 .map(|rows| Value::List(Arc::new(rows)))
540 .map_err(map_err),
541 // `List<List<…>>`: the inner inline-fixed scalar case keeps the
542 // dedicated `read_list_list` reader; `List<List<String|Schema>>`
543 // routes through the recursive `read_list_value` one level
544 // deeper. Byte-identical to both backends' prior arms.
545 TypeRepr::List { element: inner } => match inner.as_ref() {
546 TypeRepr::Int | TypeRepr::Float | TypeRepr::Bool => reader
547 .read_list_list(&field.name)
548 .map(|rows| {
549 Value::List(Arc::new(
550 rows.into_iter().map(|r| Value::List(Arc::new(r))).collect(),
551 ))
552 })
553 .map_err(map_err),
554 _ => reader
555 .read_list_value(&field.name, element.as_ref())
556 .map(|rows| Value::List(Arc::new(rows)))
557 .map_err(map_err),
558 },
559 other => Err(RuntimeError::IoError(format!(
560 "{backend} object return: cannot decode list field `{field}` of element type \
561 `{ty:?}` in schema `{schema}`",
562 field = field.name,
563 ty = other,
564 schema = parent_schema.name,
565 ))),
566 },
567 // ----- add new object-return field type arm above this line -----
568 other => Err(RuntimeError::IoError(format!(
569 "{backend} object return: cannot decode field `{field}` of type `{ty:?}` in schema \
570 `{schema}`",
571 field = field.name,
572 ty = other,
573 schema = parent_schema.name,
574 ))),
575 }
576}
577
578/// Drain every field of `schema` from the sub-record `reader` into a
579/// sorted `BTreeMap<SmolStr, Value>` — the branded-dict body for one
580/// `List<Schema>` element. Mirrors the codegen evaluators'
581/// `read_record_into_map`, but lives here so both AOT backends share the
582/// one in-place sub-record decode. The field decode reuses the existing
583/// [`BufferReader`] field-slot readers, which the verifier has already
584/// proven stay in-region for every pointer they follow.
585fn read_record_into_branded_map(
586 backend: &str,
587 reader: &BufferReader<'_>,
588 schema: &Schema,
589) -> Result<std::collections::BTreeMap<SmolStr, Value>, RuntimeError> {
590 let mut map = std::collections::BTreeMap::new();
591 for field in &schema.fields {
592 let value = read_record_field(backend, reader, field)?;
593 map.insert(SmolStr::from(field.name.as_str()), value);
594 }
595 Ok(map)
596}
597
598/// Decode one sub-record field (`reader` is the per-element sub-record
599/// reader) into a [`Value`]. Covers the scalar leaves plus every
600/// pointer-indirect list field a `List<Schema>` element can carry —
601/// **recursively, to any depth** (F7): `String`, `List<scalar>`,
602/// `List<String>`, and (new) `List<Schema>` / `List<List<…>>` element
603/// fields whose own element schemas again carry such fields. The list
604/// arms route through the buffer reader's unified recursive list reader
605/// ([`BufferReader::read_list_value`]), which follows the pointer array
606/// one level per element type — exactly the depth the multi-region
607/// verifier already certified before this decode runs. The produced
608/// values are bit-identical to the codegen evaluators' `read_list_value`
609/// path the tree-walk oracle's writer feeds. A bare nested `Schema`
610/// field, or any non-list non-scalar leaf, is capped at lowering, so a
611/// reach here is ABI drift surfaced loudly rather than mis-decoded.
612fn read_record_field(
613 backend: &str,
614 reader: &BufferReader<'_>,
615 field: &Field,
616) -> Result<Value, RuntimeError> {
617 let name = field.name.as_str();
618 let map_err = |e: crate::buffer::BufferError| {
619 RuntimeError::IoError(format!(
620 "{backend} in-place List<Schema> field `{name}`: {e}"
621 ))
622 };
623 match &field.ty {
624 TypeRepr::Int => reader.read_int(name).map(Value::Int).map_err(map_err),
625 TypeRepr::Float => reader
626 .read_float(name)
627 .map(|f| Value::Float(ordered_float::OrderedFloat(f)))
628 .map_err(map_err),
629 TypeRepr::Bool => reader.read_bool(name).map(Value::Bool).map_err(map_err),
630 TypeRepr::Unit => reader
631 .read_unit(name)
632 .map(|()| Value::option_none())
633 .map_err(map_err),
634 TypeRepr::String => reader
635 .read_string(name)
636 .map(|s| Value::String(s.into()))
637 .map_err(map_err),
638 TypeRepr::Option { .. } | TypeRepr::Result { .. } | TypeRepr::Enum { .. } => {
639 reader.read_value(name, &field.ty).map_err(map_err)
640 }
641 // Every list field — `List<scalar>` / `List<String>` /
642 // `List<Schema>` / `List<List<…>>` — decodes through the unified
643 // recursive list reader. It dispatches on `element` and recurses
644 // per pointer-array entry (into sub-records via `read_record_at`,
645 // into inner lists via itself), so nested object arrays and nested
646 // lists inside the element schema are decoded to any depth.
647 TypeRepr::List { element } => reader
648 .read_list_value(name, element.as_ref())
649 .map(|rows| Value::List(Arc::new(rows)))
650 .map_err(map_err),
651 other => Err(RuntimeError::IoError(format!(
652 "{backend} in-place List<Schema> sub-record field `{name}` has unsupported type \
653 {other:?}"
654 ))),
655 }
656}