Skip to main content

logicaffeine_compile/
interpreter.rs

1//! Tree-walking interpreter for LOGOS imperative code.
2//!
3//! This module provides runtime execution of parsed LOGOS programs by
4//! walking the AST and executing statements/expressions directly. The
5//! interpreter is async-capable to support VFS operations.
6//!
7//! # Architecture
8//!
9//! ```text
10//! LOGOS AST
11//!     │
12//!     ▼
13//! ┌────────────┐
14//! │ Interpreter│ ──▶ Evaluate expressions
15//! │            │ ──▶ Execute statements
16//! │            │ ──▶ Manage scopes
17//! └────────────┘
18//!     │
19//!     ▼
20//! RuntimeValue results
21//! ```
22//!
23//! # Runtime Values
24//!
25//! The interpreter uses [`RuntimeValue`] to represent all values at runtime:
26//! - Primitives: `Int`, `Float`, `Bool`, `Text`, `Char`
27//! - Collections: `List`, `Tuple`, `Set`, `Map`
28//! - User types: `Struct`, `Inductive` (kernel-defined types)
29//!
30//! # Async Support
31//!
32//! The interpreter is async to support VFS file operations (OPFS on WASM,
33//! `tokio::fs` on native). All statement execution is `async fn`.
34
35use std::collections::HashMap;
36use std::sync::Arc;
37use std::rc::Rc;
38use std::cell::RefCell;
39
40use async_recursion::async_recursion;
41
42use crate::ast::stmt::{BinaryOpKind, Block, ClosureBody, CompressionCodec, Expr, Literal, MatchArm, ReadSource, Stmt, TypeExpr};
43use crate::intern::{Interner, Symbol};
44
45/// Map a surface `Send compressed with <codec>` choice to the wire codec.
46fn wire_compression_of(codec: CompressionCodec) -> crate::concurrency::marshal::WireCompression {
47    use crate::concurrency::marshal::WireCompression;
48    match codec {
49        CompressionCodec::Deflate => WireCompression::Deflate,
50        CompressionCodec::Lz4 => WireCompression::Lz4,
51        CompressionCodec::Zstd => WireCompression::Zstd,
52    }
53}
54use crate::analysis::{PolicyRegistry, PolicyCondition};
55
56// VFS imports for async file operations
57use logicaffeine_system::fs::Vfs;
58use logicaffeine_runtime::{ChanId, RtPayload, SelectArm, TaskId};
59use crate::concurrency::bridge::{BlockingRequest, Yield, YieldFuture, YieldState};
60use crate::concurrency::driver::{ErrSink, InterpreterTask};
61use crate::concurrency::marshal;
62
63/// Callback type for streaming output from the interpreter.
64/// Called each time `Show` executes with the output line.
65pub type OutputCallback = Rc<RefCell<dyn FnMut(String)>>;
66
67/// Runtime values during LOGOS interpretation.
68///
69/// Represents all possible values that can exist at runtime when executing
70/// a LOGOS program. Includes primitives, collections, user-defined structs,
71/// and kernel inductive types.
72/// User-defined struct with named fields (boxed to reduce enum size).
73#[derive(Debug, Clone)]
74pub struct StructValue {
75    pub type_name: String,
76    pub fields: HashMap<String, RuntimeValue>,
77}
78
79/// Kernel inductive value (boxed to reduce enum size).
80#[derive(Debug, Clone)]
81pub struct InductiveValue {
82    pub inductive_type: String,
83    pub constructor: String,
84    pub args: Vec<RuntimeValue>,
85}
86
87/// First-class closure value (boxed to reduce enum size).
88#[derive(Debug, Clone)]
89pub struct ClosureValue {
90    pub body_index: usize,
91    pub captured_env: HashMap<Symbol, RuntimeValue>,
92    pub param_names: Vec<Symbol>,
93    /// A SHIPPED pure function carries its sandboxed generator body directly (self-
94    /// contained — no `body_index` into any arena), so it can cross the wire and be
95    /// invoked on a receiver that never compiled it. `None` for ordinary closures, whose
96    /// body lives in `closure_bodies[body_index]`. Set only by `T_FUNC` decode (and the
97    /// gated `Send computed` lowering); the call path evaluates it via the sandbox.
98    pub generated: Option<std::rc::Rc<crate::concurrency::marshal::GenExpr>>,
99}
100
101/// The Map payload behind `RuntimeValue::Map`. INSERTION-ORDERED (`IndexMap`):
102/// iteration, display, and marshaling follow the order keys were first
103/// inserted — the LOGOS `Map` contract, identical across the tree-walker, the
104/// VM, the AOT `LogosMap`, and the direct-WASM linear map. FxHash instead of
105/// the standard library's SipHash: map-heavy programs hash on every
106/// get/insert, and the keys here are small values (ints, short texts) where
107/// Fx is several times faster — with no DoS-resistance requirement (a
108/// single-program interpreter hashing its own program's keys). NOTE: removal
109/// must go through `shift_remove` (order-preserving), never `swap_remove`.
110pub type MapStorage =
111    indexmap::IndexMap<RuntimeValue, RuntimeValue, std::hash::BuildHasherDefault<rustc_hash::FxHasher>>;
112
113/// The List payload behind `RuntimeValue::List`: homogeneous all-Int and
114/// all-Float lists store UNBOXED vectors (cache-dense, and the JIT can pin a
115/// raw pointer to them); anything else boxes. The repr lives INSIDE the
116/// `Rc<RefCell<…>>`, so promotion re-tags the payload in place and every
117/// alias observes it — reference semantics and Rc identity are untouched.
118/// An EMPTY list is vacuously `Ints` and re-tags freely on its first push.
119///
120/// Hot paths take `&self`/`&mut self` borrows only — no Rc refcount traffic.
121/// `Clone` snapshots a buffer's contents for the region deopt-rollback of an
122/// in-place-mutated array (see `crate::vm::native_tier::ArrayPin::mutated`).
123#[derive(Debug, Clone)]
124pub enum ListRepr {
125    Boxed(Vec<RuntimeValue>),
126    Ints(Vec<i64>),
127    /// A proven-narrowable Int buffer stored half-width: every element fits
128    /// `i32` (the narrowing proof in `codegen::narrow`), so the buffer is
129    /// `Vec<i32>` — half the footprint and cache pressure. Reads SIGN-EXTEND
130    /// (`x as i64`, lossless); writes TRUNCATE (`x as i32`, lossless *because*
131    /// of the proof, debug-asserted in range). A write outside `i32` range
132    /// PROMOTES the buffer back to a full-width `Ints` (the proof was wrong —
133    /// soundness over speed), so observable values never differ from `Ints`.
134    /// Created only behind `LOGOS_NARROW_VM` for narrowable declarations.
135    IntsI32(Vec<i32>),
136    Floats(Vec<f64>),
137    Bools(Vec<bool>),
138    /// A flat string array (Arrow-style): all element bytes concatenated in one
139    /// `data` buffer, with `ends[i]` the exclusive end offset of element `i` (so
140    /// element `i` is `data[ends[i-1]..ends[i]]`, `ends.len()` == the count). This
141    /// is the columnar layout that lets a string list pack and load as two bulk
142    /// copies instead of one heap allocation per string — the same treatment the
143    /// numeric variants already get. It is read-optimized: an element materializes
144    /// to `Text` only when accessed, and any mutation promotes the buffer to
145    /// `Boxed`. The wire decoder builds it directly; normal code paths are
146    /// unaffected (a string *literal* stays `Boxed`).
147    ///
148    /// `cache` is a LAZY memo: empty until the first `get`, so ship/load/iterate
149    /// pay nothing, and repeated indexing of the same element returns a cheap `Rc`
150    /// clone instead of re-materializing — best of both worlds (flat to move,
151    /// boxed-cheap to re-read).
152    Strings { data: Vec<u8>, ends: Vec<u32>, cache: RefCell<Vec<Option<Rc<String>>>> },
153    /// A homogeneous struct list stored COLUMNAR (struct-of-arrays): instead of N
154    /// boxed `StructValue`s (each a heap `Box` + a `HashMap`), the schema is held
155    /// once (`type_name` + canonical sorted `field_names`) and each field becomes a
156    /// packed column — itself a `ListRepr`, so an int field is a `Vec<i64>`, a bool
157    /// field is bit-packed, a nested struct field is recursively columnar. Zero
158    /// per-row `Box`/`HashMap`; field access is a column index; the wire encoder
159    /// memcpy-streams the columns. Reads reconstruct a `StructValue` on demand
160    /// (`get`); ANY mutation de-columnarizes to `Boxed` first (`make_boxed`), so
161    /// reference semantics are exactly those of a boxed list. Built by `from_values`
162    /// for genuinely-homogeneous struct lists (same type, same field set); ragged
163    /// lists stay `Boxed`. `columns` are all the same length (the row count).
164    Structs { type_name: String, field_names: Vec<String>, columns: Vec<ListRepr> },
165    /// A homogeneous inductive (enum/ADT) list stored COLUMNAR as a tagged union:
166    /// instead of N boxed `InductiveValue`s, the type name is held once, the distinct
167    /// constructor names are dictionaried (`ctor_dict`), each row carries a small
168    /// constructor index (`ctors`), and the constructor ARGUMENTS are packed DENSE
169    /// per constructor — `arg_cols[c][j]` is the column of argument `j` across only
170    /// the rows whose constructor is `c` (so `arg_cols[c].len()` is constructor `c`'s
171    /// arity, and each inner column's length is the count of rows with constructor
172    /// `c`). `ranks[i]` is row `i`'s rank within its constructor, so `get` is O(1).
173    /// A nullary enum collapses to just the dictionary + index column (no arg cols).
174    /// Built by `from_values` for same-type enum lists; ragged/mixed-type lists stay
175    /// `Boxed`. Any mutation de-columnarizes via `make_boxed`.
176    Inductives {
177        inductive_type: String,
178        ctor_dict: Vec<String>,
179        ctors: Vec<u32>,
180        ranks: Vec<u32>,
181        arg_cols: Vec<Vec<ListRepr>>,
182    },
183    /// A received record-list held as RAW WIRE BYTES, decoded LAZILY — the production zero-copy
184    /// receive (Cap'n Proto's "read in place, only what you touch"). `bytes` is the full received
185    /// frame whose top-level value is a `T_STRUCTS_VIEW` record list; the schema (`type_name`,
186    /// `field_names`, `len`) is read ONCE from the header, so `len` and shape are O(1) with ZERO
187    /// rows decoded. Field access reads a single cell in place via `WireView::structs_row_field`
188    /// (O(1), no allocation for the untouched rows/fields); `get(i)` reconstructs one `StructValue`
189    /// on demand. ANY mutation de-lazies to `Boxed` first (`make_boxed`), so reference semantics are
190    /// exactly those of a boxed list. Built only by the `view` receive path; normal code is
191    /// unaffected.
192    WireStructs {
193        bytes: Rc<Vec<u8>>,
194        type_name: String,
195        field_names: Vec<String>,
196        len: usize,
197    },
198    /// A received NUMERIC column (`T_INTS_ALIGNED`/`T_FLOATS_ALIGNED`) held as RAW WIRE BYTES and
199    /// read ZERO-COPY: the 8-byte-aligned blob is the array. `len` is O(1) from the header (no
200    /// decode); `get(i)` reads element `i` straight out of the borrowed `&[i64]`/`&[f64]` (capnp's
201    /// `List<i64>` read-in-place); a partial read touches only what it reads. ANY mutation
202    /// materializes to `Ints`/`Floats` first. Built only by the `view` receive path for an aligned
203    /// column whose blob is 8-aligned in the received buffer (else the caller decodes eagerly).
204    WireColumn {
205        bytes: Rc<Vec<u8>>,
206        len: usize,
207        floats: bool,
208    },
209}
210
211impl ListRepr {
212    /// Wrap a received record-list frame (`T_STRUCTS_VIEW` top-level) as a lazy zero-copy backing.
213    /// `None` if the bytes are not a self-describing record-list view (the caller decodes eagerly).
214    pub fn from_record_list_view(bytes: Rc<Vec<u8>>) -> Option<ListRepr> {
215        let (type_name, field_names, len) = {
216            let view = crate::concurrency::marshal::view_message(&bytes)?;
217            view.structs_schema()?
218        };
219        Some(ListRepr::WireStructs { bytes, type_name, field_names, len })
220    }
221
222    /// Wrap a received aligned numeric column (`T_INTS_ALIGNED`/`T_FLOATS_ALIGNED`) as a lazy
223    /// zero-copy backing. `None` unless the blob reads as an 8-aligned `&[i64]`/`&[f64]` in this
224    /// buffer (so every later read is a sound slice cast); the caller then decodes eagerly.
225    pub fn from_aligned_column_view(bytes: Rc<Vec<u8>>) -> Option<ListRepr> {
226        let view = crate::concurrency::marshal::view_message(&bytes)?;
227        if let Some(s) = view.as_i64_slice() {
228            return Some(ListRepr::WireColumn { len: s.len(), floats: false, bytes });
229        }
230        if let Some(s) = view.as_f64_slice() {
231            return Some(ListRepr::WireColumn { len: s.len(), floats: true, bytes });
232        }
233        None
234    }
235
236    /// Wrap ANY received self-describing view (record list OR aligned numeric column) as a lazy
237    /// zero-copy backing, or `None` for anything else (the caller decodes eagerly).
238    pub fn from_received_view(bytes: Rc<Vec<u8>>) -> Option<ListRepr> {
239        Self::from_record_list_view(bytes.clone()).or_else(|| Self::from_aligned_column_view(bytes))
240    }
241
242    fn wire_column_get(bytes: &[u8], floats: bool, i: usize) -> Option<RuntimeValue> {
243        let view = crate::concurrency::marshal::view_message(bytes)?;
244        if floats {
245            view.as_f64_slice()?.get(i).map(|&f| RuntimeValue::Float(f))
246        } else {
247            view.as_i64_slice()?.get(i).map(|&n| RuntimeValue::Int(n))
248        }
249    }
250
251    fn wire_column_to_values(bytes: &[u8], floats: bool) -> Vec<RuntimeValue> {
252        let Some(view) = crate::concurrency::marshal::view_message(bytes) else { return Vec::new() };
253        if floats {
254            view.as_f64_slice().map(|s| s.iter().map(|&f| RuntimeValue::Float(f)).collect()).unwrap_or_default()
255        } else {
256            view.as_i64_slice().map(|s| s.iter().map(|&n| RuntimeValue::Int(n)).collect()).unwrap_or_default()
257        }
258    }
259
260    /// Materialize a single row of a lazy `WireStructs` into an owned `StructValue` by reading each
261    /// field cell in place. Shared by `get` and `make_boxed`.
262    fn wire_struct_row(
263        bytes: &[u8],
264        type_name: &str,
265        field_names: &[String],
266        len: usize,
267        i: usize,
268    ) -> Option<RuntimeValue> {
269        if i >= len {
270            return None;
271        }
272        let view = crate::concurrency::marshal::view_message(bytes)?;
273        let mut fields = HashMap::with_capacity(field_names.len());
274        for name in field_names {
275            let cell = view.structs_row_field_value(i, name)?;
276            fields.insert(name.clone(), cell);
277        }
278        Some(RuntimeValue::Struct(Box::new(StructValue { type_name: type_name.to_string(), fields })))
279    }
280}
281
282impl ListRepr {
283    pub fn from_values(values: Vec<RuntimeValue>) -> ListRepr {
284        if values.iter().all(|v| matches!(v, RuntimeValue::Int(_))) {
285            ListRepr::Ints(
286                values
287                    .into_iter()
288                    .map(|v| match v {
289                        RuntimeValue::Int(n) => n,
290                        _ => unreachable!(),
291                    })
292                    .collect(),
293            )
294        } else if values.iter().all(|v| matches!(v, RuntimeValue::Float(_))) {
295            ListRepr::Floats(
296                values
297                    .into_iter()
298                    .map(|v| match v {
299                        RuntimeValue::Float(f) => f,
300                        _ => unreachable!(),
301                    })
302                    .collect(),
303            )
304        } else if values.iter().all(|v| matches!(v, RuntimeValue::Bool(_))) {
305            ListRepr::Bools(
306                values
307                    .into_iter()
308                    .map(|v| match v {
309                        RuntimeValue::Bool(b) => b,
310                        _ => unreachable!(),
311                    })
312                    .collect(),
313            )
314        } else if !values.is_empty() && values.iter().all(|v| matches!(v, RuntimeValue::Text(_))) {
315            // A homogeneous string list de-boxes to one flat contiguous buffer (bytes + end
316            // offsets). It encodes as a single memcpy of the bytes — and the wire form is
317            // byte-identical to the boxed path — instead of one scattered copy per string.
318            let mut data = Vec::new();
319            let mut ends = Vec::with_capacity(values.len());
320            for v in &values {
321                if let RuntimeValue::Text(s) = v {
322                    data.extend_from_slice(s.as_bytes());
323                    ends.push(data.len() as u32);
324                }
325            }
326            ListRepr::strings(data, ends)
327        } else if let Some((type_name, field_names)) = Self::struct_schema(&values) {
328            // A homogeneous struct list de-boxes to columns: one packed `ListRepr`
329            // per field (recursively, so nested structs stay columnar too).
330            let columns = field_names
331                .iter()
332                .map(|fname| {
333                    ListRepr::from_values(
334                        values
335                            .iter()
336                            .map(|v| match v {
337                                RuntimeValue::Struct(sv) => sv.fields.get(fname).cloned().unwrap(),
338                                _ => unreachable!("struct_schema guaranteed all-struct"),
339                            })
340                            .collect(),
341                    )
342                })
343                .collect();
344            ListRepr::Structs { type_name, field_names, columns }
345        } else if let Some(inductives) = Self::build_inductives(&values) {
346            inductives
347        } else {
348            ListRepr::Boxed(values)
349        }
350    }
351
352    /// Build a columnar [`ListRepr::Inductives`] from a homogeneous enum list (all
353    /// the same `inductive_type`, each constructor used at a consistent arity).
354    /// `None` if not a uniform enum list (the list stays boxed). Arguments are
355    /// grouped DENSE per constructor and each group packed via `from_values`.
356    pub(crate) fn build_inductives(values: &[RuntimeValue]) -> Option<ListRepr> {
357        let inductive_type = match values.first()? {
358            RuntimeValue::Inductive(i) => i.inductive_type.clone(),
359            _ => return None,
360        };
361        let mut ctor_dict: Vec<String> = Vec::new();
362        let mut ctors: Vec<u32> = Vec::with_capacity(values.len());
363        let mut ranks: Vec<u32> = Vec::with_capacity(values.len());
364        let mut counts: Vec<u32> = Vec::new();
365        // grouped[c][j] = the j-th argument across rows whose constructor is `c`.
366        let mut grouped: Vec<Vec<Vec<RuntimeValue>>> = Vec::new();
367        for v in values {
368            let iv = match v {
369                RuntimeValue::Inductive(i) if i.inductive_type == inductive_type => i,
370                _ => return None,
371            };
372            let c = match ctor_dict.iter().position(|n| n == &iv.constructor) {
373                Some(c) => {
374                    if grouped[c].len() != iv.args.len() {
375                        return None; // a constructor used at inconsistent arity
376                    }
377                    c
378                }
379                None => {
380                    ctor_dict.push(iv.constructor.clone());
381                    counts.push(0);
382                    grouped.push(vec![Vec::new(); iv.args.len()]);
383                    ctor_dict.len() - 1
384                }
385            };
386            ctors.push(c as u32);
387            ranks.push(counts[c]);
388            counts[c] += 1;
389            for (j, a) in iv.args.iter().enumerate() {
390                grouped[c][j].push(a.clone());
391            }
392        }
393        let arg_cols: Vec<Vec<ListRepr>> = grouped
394            .into_iter()
395            .map(|cols| cols.into_iter().map(ListRepr::from_values).collect())
396            .collect();
397        Some(ListRepr::Inductives { inductive_type, ctor_dict, ctors, ranks, arg_cols })
398    }
399
400    /// Reconstruct row `i` of a columnar enum store as a boxed `InductiveValue`.
401    fn inductive_row(
402        inductive_type: &str,
403        ctor_dict: &[String],
404        ctors: &[u32],
405        ranks: &[u32],
406        arg_cols: &[Vec<ListRepr>],
407        i: usize,
408    ) -> Option<RuntimeValue> {
409        let c = *ctors.get(i)? as usize;
410        let r = ranks[i] as usize;
411        let mut args = Vec::with_capacity(arg_cols[c].len());
412        for col in &arg_cols[c] {
413            args.push(col.get(r)?);
414        }
415        Some(RuntimeValue::Inductive(Box::new(InductiveValue {
416            inductive_type: inductive_type.to_string(),
417            constructor: ctor_dict[c].clone(),
418            args,
419        })))
420    }
421
422    /// If `values` is a non-empty run of structs that all share one `type_name` and
423    /// the same field-name set, return `(type_name, sorted_field_names)` — the schema
424    /// for a columnar [`ListRepr::Structs`]. `None` otherwise (the list stays boxed).
425    /// Fields are sorted so the columnar order is canonical and stable.
426    fn struct_schema(values: &[RuntimeValue]) -> Option<(String, Vec<String>)> {
427        let first = match values.first()? {
428            RuntimeValue::Struct(s) => s,
429            _ => return None,
430        };
431        let mut names: Vec<String> = first.fields.keys().cloned().collect();
432        names.sort();
433        // A columnar store needs ≥1 column to carry the row count — a zero-field
434        // struct list stays boxed.
435        if names.is_empty() {
436            return None;
437        }
438        for item in values {
439            match item {
440                RuntimeValue::Struct(s)
441                    if s.type_name == first.type_name
442                        && s.fields.len() == names.len()
443                        && names.iter().all(|n| s.fields.contains_key(n)) => {}
444                _ => return None,
445            }
446        }
447        Some((first.type_name.clone(), names))
448    }
449
450    /// Reconstruct row `i` of a columnar struct store as a boxed `StructValue`.
451    fn struct_row(type_name: &str, field_names: &[String], columns: &[ListRepr], i: usize) -> Option<RuntimeValue> {
452        if i >= columns.first().map_or(0, |c| c.len()) {
453            return None;
454        }
455        let mut fields = std::collections::HashMap::with_capacity(field_names.len());
456        for (j, fname) in field_names.iter().enumerate() {
457            fields.insert(fname.clone(), columns[j].get(i)?);
458        }
459        Some(RuntimeValue::Struct(Box::new(StructValue { type_name: type_name.to_string(), fields })))
460    }
461
462    /// A flat string buffer with an empty (lazy) materialization cache.
463    pub fn strings(data: Vec<u8>, ends: Vec<u32>) -> ListRepr {
464        ListRepr::Strings { data, ends, cache: RefCell::new(Vec::new()) }
465    }
466
467    /// Element `i` of a flat `Strings` buffer as an owned `String` (UTF-8 was
468    /// validated when the buffer was built, but we re-check rather than risk UB).
469    fn string_at(data: &[u8], ends: &[u32], i: usize) -> Option<String> {
470        let end = *ends.get(i)? as usize;
471        let start = if i == 0 { 0 } else { ends[i - 1] as usize };
472        std::str::from_utf8(data.get(start..end)?).ok().map(str::to_string)
473    }
474
475    pub fn len(&self) -> usize {
476        match self {
477            ListRepr::Boxed(v) => v.len(),
478            ListRepr::Ints(v) => v.len(),
479            ListRepr::IntsI32(v) => v.len(),
480            ListRepr::Floats(v) => v.len(),
481            ListRepr::Bools(v) => v.len(),
482            ListRepr::Strings { ends, .. } => ends.len(),
483            ListRepr::Structs { columns, .. } => columns.first().map_or(0, |c| c.len()),
484            ListRepr::Inductives { ctors, .. } => ctors.len(),
485            ListRepr::WireStructs { len, .. } | ListRepr::WireColumn { len, .. } => *len,
486        }
487    }
488
489    pub fn is_empty(&self) -> bool {
490        self.len() == 0
491    }
492
493    /// A human label for the underlying storage layout — for the debugger's memory
494    /// view. It teaches that a list of ints is a *packed* `Vec<i64>` (not boxed
495    /// values), a struct list is columnar (struct-of-arrays), and so on: the dense
496    /// representations the VM picks automatically.
497    pub fn storage_label(&self) -> &'static str {
498        match self {
499            ListRepr::Boxed(_) => "boxed values",
500            ListRepr::Ints(_) => "packed Vec<i64>",
501            ListRepr::IntsI32(_) => "packed Vec<i32> (narrowed)",
502            ListRepr::Floats(_) => "packed Vec<f64>",
503            ListRepr::Bools(_) => "packed Vec<bool>",
504            ListRepr::Strings { .. } => "flat string buffer",
505            ListRepr::Structs { .. } => "columnar (struct-of-arrays)",
506            ListRepr::Inductives { .. } => "columnar tagged-union",
507            ListRepr::WireStructs { .. } | ListRepr::WireColumn { .. } => "wire view (lazy)",
508        }
509    }
510
511    /// Drop every element past `n` (no-op when already `<= n`). The region
512    /// deopt path uses this to roll a pushed buffer back to its entry length
513    /// so discard-and-replay re-pushes cleanly instead of duplicating.
514    pub fn truncate(&mut self, n: usize) {
515        match self {
516            ListRepr::Boxed(v) => v.truncate(n),
517            ListRepr::Ints(v) => v.truncate(n),
518            ListRepr::IntsI32(v) => v.truncate(n),
519            ListRepr::Floats(v) => v.truncate(n),
520            ListRepr::Bools(v) => v.truncate(n),
521            ListRepr::Strings { data, ends, cache } => {
522                if n < ends.len() {
523                    let cut = if n == 0 { 0 } else { ends[n - 1] as usize };
524                    data.truncate(cut);
525                    ends.truncate(n);
526                    let mut c = cache.borrow_mut();
527                    if !c.is_empty() {
528                        c.truncate(n);
529                    }
530                }
531            }
532            ListRepr::Structs { columns, .. } => {
533                for c in columns.iter_mut() {
534                    c.truncate(n);
535                }
536            }
537            // The union's arg columns are dense (not row-aligned), so a row-range
538            // truncate de-columnarizes first (a rare path — region rollback).
539            ListRepr::Inductives { .. } => {
540                if n < self.len() {
541                    self.make_boxed().truncate(n);
542                }
543            }
544            // A structural mutation de-lazies the received view first.
545            ListRepr::WireStructs { .. } | ListRepr::WireColumn { .. } => {
546                if n < self.len() {
547                    self.make_boxed().truncate(n);
548                }
549            }
550        }
551    }
552
553    /// 0-based read; boxes the scalar (stack-only — no heap, no Rc traffic).
554    pub fn get(&self, i: usize) -> Option<RuntimeValue> {
555        match self {
556            ListRepr::Boxed(v) => v.get(i).cloned(),
557            ListRepr::Ints(v) => v.get(i).map(|&n| RuntimeValue::Int(n)),
558            ListRepr::IntsI32(v) => v.get(i).map(|&n| RuntimeValue::Int(n as i64)),
559            ListRepr::Floats(v) => v.get(i).map(|&f| RuntimeValue::Float(f)),
560            ListRepr::Bools(v) => v.get(i).map(|&b| RuntimeValue::Bool(b)),
561            ListRepr::Strings { data, ends, cache } => {
562                if i >= ends.len() {
563                    return None;
564                }
565                let mut c = cache.borrow_mut();
566                // Lazily size the memo on first access — ship/load/iterate never
567                // touch it, so they pay nothing.
568                if c.is_empty() {
569                    c.resize(ends.len(), None);
570                }
571                if c[i].is_none() {
572                    c[i] = Some(Rc::new(Self::string_at(data, ends, i)?));
573                }
574                c[i].clone().map(RuntimeValue::Text)
575            }
576            ListRepr::Structs { type_name, field_names, columns } => {
577                Self::struct_row(type_name, field_names, columns, i)
578            }
579            ListRepr::Inductives { inductive_type, ctor_dict, ctors, ranks, arg_cols } => {
580                Self::inductive_row(inductive_type, ctor_dict, ctors, ranks, arg_cols, i)
581            }
582            ListRepr::WireStructs { bytes, type_name, field_names, len } => {
583                Self::wire_struct_row(bytes, type_name, field_names, *len, i)
584            }
585            ListRepr::WireColumn { bytes, floats, .. } => Self::wire_column_get(bytes, *floats, i),
586        }
587    }
588
589    /// Read field `name` of row `i` from a columnar struct list by indexing ONE
590    /// column directly — no `StructValue` reconstruction. `None` for a non-columnar
591    /// repr or a missing field/row. This is the zero-alloc read path that makes a
592    /// field scan over a columnar struct list run at array speed.
593    pub fn get_field(&self, i: usize, name: &str) -> Option<RuntimeValue> {
594        match self {
595            ListRepr::Structs { field_names, columns, .. } => {
596                let j = field_names.iter().position(|f| f == name)?;
597                columns[j].get(i)
598            }
599            // The lazy zero-copy receive: locate and decode JUST this cell in place — no row
600            // reconstruction, no decode of the other rows/fields.
601            ListRepr::WireStructs { bytes, .. } => {
602                crate::concurrency::marshal::view_message(bytes)?.structs_row_field_value(i, name)
603            }
604            _ => None,
605        }
606    }
607
608    /// Direct access to a struct field's whole packed column (the array behind a
609    /// field), for aggregating one field across the list at array speed. `None` for
610    /// a non-columnar repr or a missing field.
611    pub fn column(&self, name: &str) -> Option<&ListRepr> {
612        match self {
613            ListRepr::Structs { field_names, columns, .. } => {
614                let j = field_names.iter().position(|f| f == name)?;
615                Some(&columns[j])
616            }
617            _ => None,
618        }
619    }
620
621    /// Re-tag to Boxed in place (aliases see it — same Rc).
622    fn make_boxed(&mut self) -> &mut Vec<RuntimeValue> {
623        match self {
624            ListRepr::Boxed(v) => v,
625            ListRepr::Ints(v) => {
626                let boxed = v.drain(..).map(RuntimeValue::Int).collect();
627                *self = ListRepr::Boxed(boxed);
628                match self {
629                    ListRepr::Boxed(v) => v,
630                    _ => unreachable!(),
631                }
632            }
633            ListRepr::Floats(v) => {
634                let boxed = v.drain(..).map(RuntimeValue::Float).collect();
635                *self = ListRepr::Boxed(boxed);
636                match self {
637                    ListRepr::Boxed(v) => v,
638                    _ => unreachable!(),
639                }
640            }
641            ListRepr::IntsI32(v) => {
642                let boxed = v.drain(..).map(|n| RuntimeValue::Int(n as i64)).collect();
643                *self = ListRepr::Boxed(boxed);
644                match self {
645                    ListRepr::Boxed(v) => v,
646                    _ => unreachable!(),
647                }
648            }
649            ListRepr::Bools(v) => {
650                let boxed = v.drain(..).map(RuntimeValue::Bool).collect();
651                *self = ListRepr::Boxed(boxed);
652                match self {
653                    ListRepr::Boxed(v) => v,
654                    _ => unreachable!(),
655                }
656            }
657            ListRepr::Strings { data, ends, .. } => {
658                let boxed = (0..ends.len())
659                    .filter_map(|i| Self::string_at(data, ends, i).map(|s| RuntimeValue::Text(Rc::new(s))))
660                    .collect();
661                *self = ListRepr::Boxed(boxed);
662                match self {
663                    ListRepr::Boxed(v) => v,
664                    _ => unreachable!(),
665                }
666            }
667            ListRepr::Structs { type_name, field_names, columns } => {
668                let n = columns.first().map_or(0, |c| c.len());
669                // Invariant (held by `from_values` and the wire decoder): every column
670                // is the same length, so no row is silently dropped on de-columnarize.
671                debug_assert!(columns.iter().all(|c| c.len() == n), "columnar struct columns must share one length");
672                let boxed = (0..n)
673                    .filter_map(|i| Self::struct_row(type_name, field_names, columns, i))
674                    .collect();
675                *self = ListRepr::Boxed(boxed);
676                match self {
677                    ListRepr::Boxed(v) => v,
678                    _ => unreachable!(),
679                }
680            }
681            ListRepr::Inductives { inductive_type, ctor_dict, ctors, ranks, arg_cols } => {
682                let n = ctors.len();
683                debug_assert_eq!(ranks.len(), n, "ranks and ctors must agree");
684                let boxed = (0..n)
685                    .filter_map(|i| Self::inductive_row(inductive_type, ctor_dict, ctors, ranks, arg_cols, i))
686                    .collect();
687                *self = ListRepr::Boxed(boxed);
688                match self {
689                    ListRepr::Boxed(v) => v,
690                    _ => unreachable!(),
691                }
692            }
693            // A mutation forces the lazy receive to fully decode (read every row in place) and
694            // re-tag to `Boxed`, so reference semantics match a boxed list from that point on.
695            ListRepr::WireStructs { bytes, type_name, field_names, len } => {
696                let len = *len;
697                let boxed: Vec<RuntimeValue> = {
698                    let view = crate::concurrency::marshal::view_message(bytes);
699                    (0..len)
700                        .filter_map(|i| {
701                            view.as_ref().and_then(|v| {
702                                let mut fields = HashMap::with_capacity(field_names.len());
703                                for name in field_names.iter() {
704                                    let cell = v.structs_row_field_value(i, name)?;
705                                    fields.insert(name.clone(), cell);
706                                }
707                                Some(RuntimeValue::Struct(Box::new(StructValue {
708                                    type_name: type_name.clone(),
709                                    fields,
710                                })))
711                            })
712                        })
713                        .collect()
714                };
715                *self = ListRepr::Boxed(boxed);
716                match self {
717                    ListRepr::Boxed(v) => v,
718                    _ => unreachable!(),
719                }
720            }
721            ListRepr::WireColumn { bytes, floats, .. } => {
722                let boxed = Self::wire_column_to_values(bytes, *floats);
723                *self = ListRepr::Boxed(boxed);
724                match self {
725                    ListRepr::Boxed(v) => v,
726                    _ => unreachable!(),
727                }
728            }
729        }
730    }
731
732    /// Re-tag a half-width `IntsI32` buffer to full-width `Ints` in place — the
733    /// soundness fallback when a value outside `i32` range reaches a narrowed
734    /// buffer (the narrowing proof was unsound for that store). Sign-extends
735    /// every existing element losslessly. After this the buffer behaves exactly
736    /// like a buffer that was never narrowed.
737    fn widen_to_ints(&mut self) -> &mut Vec<i64> {
738        match self {
739            ListRepr::IntsI32(v) => {
740                let wide: Vec<i64> = v.drain(..).map(|n| n as i64).collect();
741                *self = ListRepr::Ints(wide);
742                match self {
743                    ListRepr::Ints(v) => v,
744                    _ => unreachable!(),
745                }
746            }
747            ListRepr::Ints(v) => v,
748            _ => unreachable!("widen_to_ints called on a non-Int buffer"),
749        }
750    }
751
752    /// 0-based write (bounds already validated by the caller); promotes on a
753    /// kind mismatch.
754    pub fn set(&mut self, i: usize, value: RuntimeValue) {
755        match (&mut *self, &value) {
756            (ListRepr::Ints(v), RuntimeValue::Int(n)) => v[i] = *n,
757            (ListRepr::IntsI32(v), RuntimeValue::Int(n)) => {
758                if let Ok(narrow) = i32::try_from(*n) {
759                    v[i] = narrow;
760                } else {
761                    // The proof said every store fits i32; this one did not.
762                    // Widen the whole buffer rather than truncate (which would
763                    // silently change the observable value). Soundness wins.
764                    self.widen_to_ints()[i] = *n;
765                }
766            }
767            (ListRepr::Floats(v), RuntimeValue::Float(f)) => v[i] = *f,
768            (ListRepr::Bools(v), RuntimeValue::Bool(b)) => v[i] = *b,
769            (ListRepr::Boxed(v), _) => v[i] = value,
770            _ => self.make_boxed()[i] = value,
771        }
772    }
773
774    pub fn push(&mut self, value: RuntimeValue) {
775        match (&mut *self, &value) {
776            (ListRepr::Ints(v), RuntimeValue::Int(n)) => v.push(*n),
777            (ListRepr::IntsI32(v), RuntimeValue::Int(n)) => match i32::try_from(*n) {
778                Ok(narrow) => v.push(narrow),
779                Err(_) => self.widen_to_ints().push(*n),
780            },
781            (ListRepr::Floats(v), RuntimeValue::Float(f)) => v.push(*f),
782            (ListRepr::Bools(v), RuntimeValue::Bool(b)) => v.push(*b),
783            (ListRepr::Boxed(v), _) => v.push(value),
784            (ListRepr::Ints(v), RuntimeValue::Float(f)) if v.is_empty() => {
785                *self = ListRepr::Floats(vec![*f]);
786            }
787            (ListRepr::Ints(v), RuntimeValue::Bool(b)) if v.is_empty() => {
788                *self = ListRepr::Bools(vec![*b]);
789            }
790            (ListRepr::Floats(v), RuntimeValue::Int(n)) if v.is_empty() => {
791                *self = ListRepr::Ints(vec![*n]);
792            }
793            (ListRepr::Floats(v), RuntimeValue::Bool(b)) if v.is_empty() => {
794                *self = ListRepr::Bools(vec![*b]);
795            }
796            (ListRepr::Bools(v), RuntimeValue::Int(n)) if v.is_empty() => {
797                *self = ListRepr::Ints(vec![*n]);
798            }
799            (ListRepr::Bools(v), RuntimeValue::Float(f)) if v.is_empty() => {
800                *self = ListRepr::Floats(vec![*f]);
801            }
802            _ => self.make_boxed().push(value),
803        }
804    }
805
806    pub fn pop(&mut self) -> Option<RuntimeValue> {
807        match self {
808            ListRepr::Boxed(v) => v.pop(),
809            ListRepr::Ints(v) => v.pop().map(RuntimeValue::Int),
810            ListRepr::IntsI32(v) => v.pop().map(|n| RuntimeValue::Int(n as i64)),
811            ListRepr::Floats(v) => v.pop().map(RuntimeValue::Float),
812            ListRepr::Bools(v) => v.pop().map(RuntimeValue::Bool),
813            ListRepr::Strings { data, ends, cache } => {
814                let last = ends.len().checked_sub(1)?;
815                let s = Self::string_at(data, ends, last)?;
816                let start = if last == 0 { 0 } else { ends[last - 1] as usize };
817                data.truncate(start);
818                ends.pop();
819                let mut c = cache.borrow_mut();
820                if !c.is_empty() {
821                    c.pop();
822                }
823                Some(RuntimeValue::Text(Rc::new(s)))
824            }
825            // Removing from a columnar store: de-columnarize, then pop.
826            ListRepr::Structs { .. } => self.make_boxed().pop(),
827            ListRepr::Inductives { .. } => self.make_boxed().pop(),
828            ListRepr::WireStructs { .. } | ListRepr::WireColumn { .. } => self.make_boxed().pop(),
829        }
830    }
831
832    pub fn insert(&mut self, i: usize, value: RuntimeValue) {
833        match (&mut *self, &value) {
834            (ListRepr::Ints(v), RuntimeValue::Int(n)) => v.insert(i, *n),
835            (ListRepr::IntsI32(v), RuntimeValue::Int(n)) => match i32::try_from(*n) {
836                Ok(narrow) => v.insert(i, narrow),
837                Err(_) => self.widen_to_ints().insert(i, *n),
838            },
839            (ListRepr::Floats(v), RuntimeValue::Float(f)) => v.insert(i, *f),
840            (ListRepr::Bools(v), RuntimeValue::Bool(b)) => v.insert(i, *b),
841            (ListRepr::Boxed(v), _) => v.insert(i, value),
842            _ => self.make_boxed().insert(i, value),
843        }
844    }
845
846    pub fn remove_at(&mut self, i: usize) -> RuntimeValue {
847        match self {
848            ListRepr::Boxed(v) => v.remove(i),
849            ListRepr::Ints(v) => RuntimeValue::Int(v.remove(i)),
850            ListRepr::IntsI32(v) => RuntimeValue::Int(v.remove(i) as i64),
851            ListRepr::Floats(v) => RuntimeValue::Float(v.remove(i)),
852            ListRepr::Bools(v) => RuntimeValue::Bool(v.remove(i)),
853            // Removal in the middle is O(n) on a flat buffer; promote and remove.
854            ListRepr::Strings { .. } => self.make_boxed().remove(i),
855            ListRepr::Structs { .. } => self.make_boxed().remove(i),
856            ListRepr::Inductives { .. } => self.make_boxed().remove(i),
857            ListRepr::WireStructs { .. } | ListRepr::WireColumn { .. } => self.make_boxed().remove(i),
858        }
859    }
860
861    /// Index of the first element `values_equal` to `needle` (the kernel's
862    /// equality: epsilon floats, cross-type never equal).
863    pub fn position(&self, needle: &RuntimeValue) -> Option<usize> {
864        match (self, needle) {
865            (ListRepr::Ints(v), RuntimeValue::Int(n)) => v.iter().position(|x| x == n),
866            (ListRepr::Ints(_), _) => None,
867            (ListRepr::IntsI32(v), RuntimeValue::Int(n)) => {
868                i32::try_from(*n).ok().and_then(|nn| v.iter().position(|x| *x == nn))
869            }
870            (ListRepr::IntsI32(_), _) => None,
871            (ListRepr::Floats(v), RuntimeValue::Float(f)) => {
872                v.iter().position(|x| (x - f).abs() < f64::EPSILON)
873            }
874            (ListRepr::Floats(_), _) => None,
875            (ListRepr::Bools(v), RuntimeValue::Bool(b)) => v.iter().position(|x| x == b),
876            (ListRepr::Bools(_), _) => None,
877            (ListRepr::Strings { data, ends, .. }, RuntimeValue::Text(t)) => {
878                (0..ends.len()).find(|&i| Self::string_at(data, ends, i).as_deref() == Some(t.as_str()))
879            }
880            (ListRepr::Strings { .. }, _) => None,
881            (ListRepr::Boxed(v), _) => {
882                v.iter().position(|x| crate::semantics::compare::values_equal(x, needle))
883            }
884            // A struct never equals a scalar needle, but a needle could be a struct;
885            // reconstruct row-by-row and compare (rare path — search over structs).
886            (ListRepr::Structs { .. }, _) => (0..self.len())
887                .find(|&i| self.get(i).is_some_and(|v| crate::semantics::compare::values_equal(&v, needle))),
888            (ListRepr::Inductives { .. }, _) => (0..self.len())
889                .find(|&i| self.get(i).is_some_and(|v| crate::semantics::compare::values_equal(&v, needle))),
890            // The lazy receive: reconstruct rows/elements on demand and compare.
891            (ListRepr::WireStructs { .. }, _) | (ListRepr::WireColumn { .. }, _) => (0..self.len())
892                .find(|&i| self.get(i).is_some_and(|v| crate::semantics::compare::values_equal(&v, needle))),
893        }
894    }
895
896    pub fn contains(&self, needle: &RuntimeValue) -> bool {
897        self.position(needle).is_some()
898    }
899
900    /// Materialize boxed values (snapshots, display, deep clones).
901    pub fn to_values(&self) -> Vec<RuntimeValue> {
902        match self {
903            ListRepr::Boxed(v) => v.clone(),
904            ListRepr::Ints(v) => v.iter().map(|&n| RuntimeValue::Int(n)).collect(),
905            ListRepr::IntsI32(v) => v.iter().map(|&n| RuntimeValue::Int(n as i64)).collect(),
906            ListRepr::Floats(v) => v.iter().map(|&f| RuntimeValue::Float(f)).collect(),
907            ListRepr::Bools(v) => v.iter().map(|&b| RuntimeValue::Bool(b)).collect(),
908            ListRepr::Strings { data, ends, .. } => (0..ends.len())
909                .filter_map(|i| Self::string_at(data, ends, i).map(|s| RuntimeValue::Text(Rc::new(s))))
910                .collect(),
911            ListRepr::Structs { type_name, field_names, columns } => {
912                let n = columns.first().map_or(0, |c| c.len());
913                (0..n).filter_map(|i| Self::struct_row(type_name, field_names, columns, i)).collect()
914            }
915            ListRepr::Inductives { inductive_type, ctor_dict, ctors, ranks, arg_cols } => (0..ctors.len())
916                .filter_map(|i| Self::inductive_row(inductive_type, ctor_dict, ctors, ranks, arg_cols, i))
917                .collect(),
918            ListRepr::WireStructs { bytes, type_name, field_names, len } => (0..*len)
919                .filter_map(|i| Self::wire_struct_row(bytes, type_name, field_names, *len, i))
920                .collect(),
921            ListRepr::WireColumn { bytes, floats, .. } => Self::wire_column_to_values(bytes, *floats),
922        }
923    }
924
925    /// 0-based inclusive-range slice as a fresh payload of the same repr.
926    pub fn slice(&self, start: usize, end: usize) -> ListRepr {
927        match self {
928            ListRepr::Boxed(v) => ListRepr::Boxed(v[start..=end].to_vec()),
929            ListRepr::Ints(v) => ListRepr::Ints(v[start..=end].to_vec()),
930            ListRepr::IntsI32(v) => ListRepr::IntsI32(v[start..=end].to_vec()),
931            ListRepr::Floats(v) => ListRepr::Floats(v[start..=end].to_vec()),
932            ListRepr::Bools(v) => ListRepr::Bools(v[start..=end].to_vec()),
933            ListRepr::Strings { data, ends, .. } => ListRepr::Boxed(
934                (start..=end)
935                    .filter_map(|i| Self::string_at(data, ends, i).map(|s| RuntimeValue::Text(Rc::new(s))))
936                    .collect(),
937            ),
938            // Slicing stays columnar — slice each column to the same range.
939            ListRepr::Structs { type_name, field_names, columns } => ListRepr::Structs {
940                type_name: type_name.clone(),
941                field_names: field_names.clone(),
942                columns: columns.iter().map(|c| c.slice(start, end)).collect(),
943            },
944            // The union's arg columns are dense, so re-columnarize the sliced rows.
945            ListRepr::Inductives { .. } => {
946                ListRepr::from_values((start..=end).filter_map(|i| self.get(i)).collect())
947            }
948            // Reconstruct just the sliced rows/elements from the received view.
949            ListRepr::WireStructs { .. } | ListRepr::WireColumn { .. } => {
950                ListRepr::from_values((start..=end).filter_map(|i| self.get(i)).collect())
951            }
952        }
953    }
954
955    /// Direct unboxed views for the JIT's region pinning.
956    pub fn as_ints_mut(&mut self) -> Option<&mut Vec<i64>> {
957        match self {
958            ListRepr::Ints(v) => Some(v),
959            _ => None,
960        }
961    }
962
963    pub fn as_floats_mut(&mut self) -> Option<&mut Vec<f64>> {
964        match self {
965            ListRepr::Floats(v) => Some(v),
966            _ => None,
967        }
968    }
969}
970
971#[derive(Debug, Clone)]
972pub enum RuntimeValue {
973    Int(i64),
974    /// An exact integer that does NOT fit `i64` — the overflow-safe continuation of
975    /// `Int`. INVARIANT: `b.to_i64().is_none()` always holds (build via
976    /// [`RuntimeValue::from_bigint`], which downsizes any in-range result back to
977    /// `Int`), so there is exactly one representation per integer value and `Eq`/
978    /// `Hash`/ordering never need a cross-`Int` arm. `Rc` keeps `Clone` O(1).
979    BigInt(Rc<logicaffeine_base::BigInt>),
980    /// An exact rational number — the result of an integer division that does NOT
981    /// divide evenly (`7 / 2 → 7/2`), the way `Int` "overflows" into `BigInt`.
982    /// INVARIANT: never a whole number — build via [`RuntimeValue::from_rational`],
983    /// which downsizes an integer-valued rational to `Int`/`BigInt`, so a value has
984    /// one canonical representation and `Eq`/`Hash` need no cross-`Int` arm.
985    Rational(Rc<logicaffeine_base::Rational>),
986    /// An exact base-10 fixed-point number — money's type. Distinct from `Rational`:
987    /// it carries a *scale* (decimal places) for faithful display (`19.99`, not
988    /// `1999/100`), and unlike `Int`/`BigInt`/`Rational` it does NOT downsize on a
989    /// whole value (`20.00` stays `Decimal`, not `Int`), because the scale is meaning.
990    /// `+ − ×` are exact and keep it `Decimal`; `÷` and a `Rational` operand promote to
991    /// the exact `Rational` (base-10 division need not terminate). `Rc` keeps `Clone` O(1).
992    Decimal(Rc<logicaffeine_base::Decimal>),
993    /// An exact complex number `re + im·i`, each part a `Rational`. The field that closes
994    /// the tower for `√` of a negative and for EE/signal math: `i·i = −1` exactly. NOT
995    /// ordered (complex numbers have no total order), so it never appears in `compare`.
996    Complex(Rc<logicaffeine_base::Complex>),
997    /// An element of the ring ℤ/nℤ — an integer modulo a fixed modulus (the arbitrary-modulus
998    /// generalisation of `Word`). Arithmetic wraps into `[0, modulus)`; the crypto/number-theory
999    /// substrate (modular exponentiation, inverse). Equal only at the same value AND modulus.
1000    Modular(Rc<logicaffeine_base::Modular>),
1001    Float(f64),
1002    Bool(bool),
1003    Text(Rc<String>),
1004    Char(char),
1005    List(Rc<RefCell<ListRepr>>),
1006    Tuple(Rc<Vec<RuntimeValue>>),
1007    Set(Rc<RefCell<Vec<RuntimeValue>>>),
1008    Map(Rc<RefCell<MapStorage>>),
1009    Struct(Box<StructValue>),
1010    Inductive(Box<InductiveValue>),
1011    Function(Box<ClosureValue>),
1012    Nothing,
1013    Duration(i64),
1014    Date(i32),
1015    Moment(i64),
1016    Span { months: i32, days: i32 },
1017    Time(i64),
1018    /// A channel handle (a `Pipe`) — an opaque token into the scheduler.
1019    Chan(ChanId),
1020    /// A spawned-task handle — an opaque token into the scheduler.
1021    TaskHandle(TaskId),
1022    /// A remote peer handle — its canonical relay topic. `Send … to <peer>`
1023    /// publishes on this topic; the peer receives it on its own inbox.
1024    Peer(Rc<String>),
1025    /// A live CRDT (observed-remove set, replicated sequence, or multi-value register)
1026    /// held by the tree-walker. Wraps the real `logicaffeine_data` type the compiled tier
1027    /// uses, so merge converges identically across tiers. `Rc<RefCell<_>>` gives the same
1028    /// interior-mutation/aliasing semantics as `Set`/`List`/`Map`, so mutating a struct's
1029    /// CRDT field through a field access updates the shared value in place.
1030    Crdt(Rc<RefCell<crate::semantics::crdt::CrdtValue>>),
1031    /// A fixed-width wrapping integer (`Word32`/`Word64`) — the ring ℤ/2ᵏ the bit-twiddling
1032    /// primitives (ChaCha20 over `Word32`, Keccak over `Word64`) compute over. Distinct from
1033    /// `Int`: its arithmetic wraps and it never promotes to `BigInt`.
1034    Word(logicaffeine_base::WordVal),
1035    /// A SIMD lane vector (`Lanes8Word32` = 8×`Word32` = one `__m256i`) — a fixed-width vector over
1036    /// the Word ring. The tree-walker carries the scalar-lane representation and computes each op as
1037    /// independent scalar lanes (the spec); AOT lowers the same op to an AVX2 intrinsic. Boxed in `Rc`
1038    /// so the 256-bit lane payload stays out of the 16-byte `RuntimeValue` (the NaN-box invariant).
1039    Lanes(Rc<logicaffeine_base::LanesVal>),
1040    /// A physical quantity — an exact magnitude carrying a `Dimension` and a display unit
1041    /// (`2 inches`, `9.8 m/s²`). The magnitude rides the exact rational tower, so unit conversion
1042    /// is lossless (`2 inches + 5 cm in feet = 42/127 ft`); `+ −` and comparison require the SAME
1043    /// dimension (else a typed error, like Word width-mismatch), `× ÷` combine dimensions. The
1044    /// display unit travels with the value so `Show` renders it faithfully.
1045    Quantity(Rc<QuantityValue>),
1046    /// An exact monetary amount in a currency (`19.99 USD`). The amount rides the Decimal tower so it
1047    /// never float-drifts; `+ −` and comparison require the SAME currency (else a typed error, like a
1048    /// dimension mismatch), `× ÷` scale by a number. The currency travels with the value.
1049    Money(Rc<logicaffeine_base::Money>),
1050    /// A 128-bit UUID (RFC 9562). `Ord` by bytes — so v6/v7 ids sort chronologically — and a stable
1051    /// canonical text form. `Rc`-boxed to keep `RuntimeValue` at 16 bytes (the value itself is a
1052    /// `Copy [u8;16]`; the compiled tier carries it unboxed).
1053    Uuid(Rc<logicaffeine_base::Uuid>),
1054}
1055
1056/// The payload of a [`RuntimeValue::Quantity`]: the physical quantity (magnitude in SI base +
1057/// dimension) plus the unit it should be displayed in. Equality/hashing are by physical value
1058/// (SI magnitude + dimension) — the display unit is presentation only, so `2 inches` equals
1059/// `5.08 centimetres`.
1060#[derive(Clone, Debug)]
1061pub struct QuantityValue {
1062    pub q: logicaffeine_base::Quantity,
1063    pub unit: logicaffeine_base::Unit,
1064}
1065
1066impl QuantityValue {
1067    /// The faithful display: the magnitude expressed in the carried unit, then its symbol —
1068    /// `42/127 ft`, `2 in`, `20 °C`. A synthetic SI unit (empty symbol, produced by a
1069    /// dimension-combining `× ÷`) shows the dimension signature instead (`12 L^2`).
1070    pub fn display(&self) -> String {
1071        let magnitude = self
1072            .q
1073            .in_unit(&self.unit)
1074            .expect("a Quantity's display unit always shares its dimension");
1075        if self.unit.symbol.is_empty() {
1076            format!("{} {}", magnitude, self.q.dimension())
1077        } else {
1078            format!("{} {}", magnitude, self.unit.symbol)
1079        }
1080    }
1081}
1082
1083impl PartialEq for RuntimeValue {
1084    /// ONE equality: delegates to [`crate::semantics::compare::values_equal`],
1085    /// so map-key lookup, set membership, and the language's `==` can never
1086    /// disagree. Structural for collections/structs, EXACT across numeric
1087    /// types (`1 == 1.0`), IEEE for floats — coherent with the unified
1088    /// numeric `Hash` below (equal values hash equal).
1089    fn eq(&self, other: &Self) -> bool {
1090        crate::semantics::compare::values_equal(self, other)
1091    }
1092}
1093
1094/// NOTE: `eq` is IEEE on floats, so `NaN != NaN` — strictly this bends `Eq`'s
1095/// reflexivity for the one value IEEE defines as not equal to itself. The
1096/// trade is deliberate: map keys behave exactly like the language's own `==`
1097/// (a NaN key is unfindable, as IEEE intends) instead of maps and `==`
1098/// silently disagreeing about float identity. Everything else is a total
1099/// equivalence.
1100impl Eq for RuntimeValue {}
1101
1102impl std::hash::Hash for RuntimeValue {
1103    /// The hash/equality coherence law: values that compare equal MUST hash
1104    /// equal. Numeric types are cross-type equal (`1 == 1.0 == 1/1`), so they
1105    /// share ONE hash stream — the unified numeric hash (value mod 2^61 − 1,
1106    /// `base::numeric`) with NO discriminant prefix. Everything else keeps
1107    /// its discriminant-prefixed per-type hash (collisions between UNEQUAL
1108    /// values are always allowed; only equal ⇒ equal-hash is required).
1109    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1110        use logicaffeine_base::numeric;
1111        match self {
1112            // ── The unified numeric stream (no discriminant) ─────────────
1113            RuntimeValue::Int(n) => state.write_u64(numeric::numeric_hash_i64(*n)),
1114            RuntimeValue::BigInt(b) => state.write_u64(numeric::numeric_hash_bigint(b)),
1115            RuntimeValue::Float(f) => state.write_u64(numeric::numeric_hash_f64(*f)),
1116            RuntimeValue::Rational(r) => state.write_u64(numeric::numeric_hash_rational(r)),
1117            // ── Discriminant-prefixed per-type hashes ────────────────────
1118            other => {
1119                std::mem::discriminant(other).hash(state);
1120                match other {
1121                    RuntimeValue::Int(_)
1122                    | RuntimeValue::BigInt(_)
1123                    | RuntimeValue::Float(_)
1124                    | RuntimeValue::Rational(_) => unreachable!("handled above"),
1125                    RuntimeValue::Decimal(d) => d.hash(state),
1126                    RuntimeValue::Complex(c) => c.hash(state),
1127                    RuntimeValue::Modular(m) => m.hash(state),
1128                    RuntimeValue::Bool(b) => b.hash(state),
1129                    RuntimeValue::Text(s) => s.hash(state),
1130                    RuntimeValue::Char(c) => c.hash(state),
1131                    RuntimeValue::Nothing => {}
1132                    RuntimeValue::Duration(d) => d.hash(state),
1133                    RuntimeValue::Date(d) => d.hash(state),
1134                    RuntimeValue::Moment(m) => m.hash(state),
1135                    RuntimeValue::Span { months, days } => { months.hash(state); days.hash(state); }
1136                    RuntimeValue::Time(t) => t.hash(state),
1137                    // Tuples are VALUE keys: content-hashed, in order —
1138                    // coherent with their structural equality.
1139                    RuntimeValue::Tuple(items) => {
1140                        items.len().hash(state);
1141                        for v in items.iter() {
1142                            v.hash(state);
1143                        }
1144                    }
1145                    // Structs are VALUE keys: type + an ORDER-INSENSITIVE
1146                    // field fold (the fields map iterates nondeterministically,
1147                    // and equal structs must hash equal).
1148                    RuntimeValue::Struct(s) => {
1149                        s.type_name.hash(state);
1150                        let mut fold: u64 = 0;
1151                        for (k, v) in &s.fields {
1152                            let mut h = rustc_hash::FxHasher::default();
1153                            std::hash::Hash::hash(k, &mut h);
1154                            std::hash::Hash::hash(v, &mut h);
1155                            fold = fold.wrapping_add(std::hash::Hasher::finish(&h));
1156                        }
1157                        state.write_u64(fold);
1158                    }
1159                    // Mutable containers hash by LENGTH — consistent with
1160                    // structural equality (equal ⇒ equal length), and they
1161                    // are rejected as map keys at insert anyway.
1162                    RuntimeValue::List(items) => items.borrow().len().hash(state),
1163                    RuntimeValue::Set(items) => items.borrow().len().hash(state),
1164                    RuntimeValue::Map(m) => m.borrow().len().hash(state),
1165                    RuntimeValue::Inductive(i) => { i.inductive_type.hash(state); i.constructor.hash(state); }
1166                    RuntimeValue::Function(f) => f.body_index.hash(state),
1167                    RuntimeValue::Chan(c) => c.0.hash(state),
1168                    RuntimeValue::TaskHandle(t) => t.0.hash(state),
1169                    RuntimeValue::Peer(topic) => topic.hash(state),
1170                    RuntimeValue::Crdt(c) => c.borrow().len().hash(state),
1171                    RuntimeValue::Word(w) => w.hash(state),
1172                    RuntimeValue::Lanes(v) => v.hash(state),
1173                    // Hash the physical value (SI magnitude + dimension), consistent with `eq`
1174                    // ignoring the display unit.
1175                    RuntimeValue::Quantity(qv) => {
1176                        qv.q.magnitude_si().hash(state);
1177                        qv.q.dimension().hash(state);
1178                    }
1179                    // Hash by value (currency + amount), consistent with `eq`.
1180                    RuntimeValue::Money(m) => m.hash(state),
1181                    RuntimeValue::Uuid(u) => u.hash(state),
1182                }
1183            }
1184        }
1185    }
1186}
1187
1188impl RuntimeValue {
1189    /// Build an integer value from a `BigInt`, DOWNSIZING to [`RuntimeValue::Int`]
1190    /// whenever the value fits `i64`. This is the single chokepoint that maintains
1191    /// the `BigInt`-is-always-out-of-range invariant, so every integer has one
1192    /// canonical representation — the "downsize when it provably fits" rule, applied
1193    /// unconditionally on every result.
1194    pub fn from_bigint(b: logicaffeine_base::BigInt) -> RuntimeValue {
1195        match b.to_i64() {
1196            Some(i) => RuntimeValue::Int(i),
1197            None => RuntimeValue::BigInt(Rc::new(b)),
1198        }
1199    }
1200
1201    /// Build a number from a `Rational`, DOWNSIZING to an exact integer
1202    /// (`Int`/`BigInt`) whenever the denominator reduces to `1`. This is the single
1203    /// chokepoint that maintains the `Rational`-is-never-whole invariant, so an
1204    /// integer-valued result (`6 / 2 → 3`) is an `Int`, not a `Rational` — exactly the
1205    /// "downsize when it provably fits" rule `from_bigint` applies for integers.
1206    pub fn from_rational(r: logicaffeine_base::Rational) -> RuntimeValue {
1207        match r.to_bigint() {
1208            Some(whole) => RuntimeValue::from_bigint(whole),
1209            None => RuntimeValue::Rational(Rc::new(r)),
1210        }
1211    }
1212
1213    /// Returns the type name of this value as a string slice.
1214    ///
1215    /// Used for error messages and type checking at runtime.
1216    pub fn type_name(&self) -> &str {
1217        match self {
1218            RuntimeValue::Int(_) => "Int",
1219            // A BigInt is an exact integer too — same logical type, wider repr — so it
1220            // reports "Int", keeping the type stable across promotion/downsizing.
1221            RuntimeValue::BigInt(_) => "Int",
1222            RuntimeValue::Rational(_) => "Rational",
1223            RuntimeValue::Decimal(_) => "Decimal",
1224            RuntimeValue::Complex(_) => "Complex",
1225            RuntimeValue::Modular(_) => "Modular",
1226            RuntimeValue::Float(_) => "Float",
1227            RuntimeValue::Bool(_) => "Bool",
1228            RuntimeValue::Text(_) => "Text",
1229            RuntimeValue::Char(_) => "Char",
1230            RuntimeValue::List(_) => "List",
1231            RuntimeValue::Tuple(_) => "Tuple",
1232            RuntimeValue::Set(_) => "Set",
1233            RuntimeValue::Map(_) => "Map",
1234            RuntimeValue::Struct(s) => &s.type_name,
1235            RuntimeValue::Inductive(ind) => ind.inductive_type.as_str(),
1236            RuntimeValue::Function(_) => "Function",
1237            RuntimeValue::Nothing => "Nothing",
1238            RuntimeValue::Duration(_) => "Duration",
1239            RuntimeValue::Date(_) => "Date",
1240            RuntimeValue::Moment(_) => "Moment",
1241            RuntimeValue::Span { .. } => "Span",
1242            RuntimeValue::Time(_) => "Time",
1243            RuntimeValue::Chan(_) => "Channel",
1244            RuntimeValue::TaskHandle(_) => "Task",
1245            RuntimeValue::Peer(_) => "PeerAgent",
1246            RuntimeValue::Crdt(c) => c.borrow().kind(),
1247            RuntimeValue::Word(w) => {
1248                if w.width() == 32 {
1249                    "Word32"
1250                } else {
1251                    "Word64"
1252                }
1253            }
1254            RuntimeValue::Lanes(v) => v.type_name(),
1255            RuntimeValue::Quantity(_) => "Quantity",
1256            RuntimeValue::Money(_) => "Money",
1257            RuntimeValue::Uuid(_) => "Uuid",
1258        }
1259    }
1260
1261    /// Checks if this value evaluates to true in a boolean context.
1262    ///
1263    /// - `Bool(true)` → true
1264    /// - `Int(n)` → true if n ≠ 0
1265    /// - `Nothing` → false
1266    /// - All other values → true
1267    pub fn deep_clone(&self) -> RuntimeValue {
1268        match self {
1269            RuntimeValue::List(items) => {
1270                let cloned: Vec<RuntimeValue> =
1271                    items.borrow().to_values().iter().map(|v| v.deep_clone()).collect();
1272                RuntimeValue::List(Rc::new(RefCell::new(ListRepr::from_values(cloned))))
1273            }
1274            RuntimeValue::Set(items) => {
1275                let cloned = items.borrow().iter().map(|v| v.deep_clone()).collect();
1276                RuntimeValue::Set(Rc::new(RefCell::new(cloned)))
1277            }
1278            RuntimeValue::Map(m) => {
1279                let cloned = m.borrow().iter().map(|(k, v)| (k.deep_clone(), v.deep_clone())).collect();
1280                RuntimeValue::Map(Rc::new(RefCell::new(cloned)))
1281            }
1282            RuntimeValue::Tuple(items) => {
1283                let cloned = items.iter().map(|v| v.deep_clone()).collect();
1284                RuntimeValue::Tuple(Rc::new(cloned))
1285            }
1286            RuntimeValue::Struct(s) => {
1287                let cloned_fields = s.fields.iter().map(|(k, v)| (k.clone(), v.deep_clone())).collect();
1288                RuntimeValue::Struct(Box::new(StructValue {
1289                    type_name: s.type_name.clone(),
1290                    fields: cloned_fields,
1291                }))
1292            }
1293            RuntimeValue::Inductive(ind) => {
1294                let cloned_args = ind.args.iter().map(|v| v.deep_clone()).collect();
1295                RuntimeValue::Inductive(Box::new(InductiveValue {
1296                    inductive_type: ind.inductive_type.clone(),
1297                    constructor: ind.constructor.clone(),
1298                    args: cloned_args,
1299                }))
1300            }
1301            RuntimeValue::Function(f) => {
1302                let cloned_env = f.captured_env.iter()
1303                    .map(|(k, v)| (k.clone(), v.deep_clone()))
1304                    .collect();
1305                RuntimeValue::Function(Box::new(ClosureValue {
1306                    body_index: f.body_index,
1307                    captured_env: cloned_env,
1308                    param_names: f.param_names.clone(),
1309                    generated: f.generated.clone(),
1310                }))
1311            }
1312            // A value-copy of a CRDT is an INDEPENDENT replica — deep-copy the inner state
1313            // so a later mutation of one copy does not alias the other (a shallow `Rc`
1314            // share would make a struct copy mutate the original's CRDT field).
1315            RuntimeValue::Crdt(c) => {
1316                RuntimeValue::Crdt(Rc::new(RefCell::new(c.borrow().clone())))
1317            }
1318            other => other.clone(),
1319        }
1320    }
1321
1322    /// Falsy: `false`, numeric zero (Int/Float/BigInt/Rational/Decimal/Complex/Word),
1323    /// `nothing`, and empty Text/List/Set/Map. Everything else is truthy.
1324    /// (`-0.0` is zero; NaN is nonzero and therefore truthy.)
1325    pub fn is_truthy(&self) -> bool {
1326        match self {
1327            RuntimeValue::Bool(b) => *b,
1328            RuntimeValue::Int(n) => *n != 0,
1329            RuntimeValue::Float(f) => *f != 0.0,
1330            RuntimeValue::BigInt(b) => !b.is_zero(),
1331            RuntimeValue::Rational(r) => !r.is_zero(),
1332            RuntimeValue::Decimal(d) => !d.is_zero(),
1333            RuntimeValue::Complex(c) => !c.is_zero(),
1334            RuntimeValue::Word(w) => w.to_u64() != 0,
1335            RuntimeValue::Text(s) => !s.is_empty(),
1336            RuntimeValue::List(l) => l.borrow().len() != 0,
1337            RuntimeValue::Set(s) => !s.borrow().is_empty(),
1338            RuntimeValue::Map(m) => !m.borrow().is_empty(),
1339            RuntimeValue::Nothing => false,
1340            _ => true,
1341        }
1342    }
1343
1344    /// Converts this value to a human-readable string for display.
1345    ///
1346    /// Used by the `show()` built-in function and for debugging output.
1347    /// Formats collections with brackets, structs with field names, and
1348    /// inductive values with constructor notation.
1349    pub fn to_display_string(&self) -> String {
1350        match self {
1351            RuntimeValue::Int(n) => n.to_string(),
1352            RuntimeValue::Word(w) => w.to_string(),
1353            // A lane vector renders like a Seq of its lanes — `[l0, l1, ...]` of unsigned values.
1354            RuntimeValue::Lanes(v) => {
1355                let parts: Vec<String> =
1356                    (0..v.lanes()).map(|i| v.lane(i).to_string()).collect();
1357                format!("[{}]", parts.join(", "))
1358            }
1359            RuntimeValue::BigInt(b) => b.to_string(),
1360            RuntimeValue::Rational(r) => r.to_string(),
1361            RuntimeValue::Decimal(d) => d.to_string(),
1362            RuntimeValue::Complex(c) => c.to_string(),
1363            RuntimeValue::Modular(m) => m.to_string(),
1364            RuntimeValue::Quantity(qv) => qv.display(),
1365            RuntimeValue::Money(m) => m.to_string(),
1366            RuntimeValue::Uuid(u) => u.to_string(),
1367            RuntimeValue::Float(f) => logicaffeine_data::fmt::fmt_f64(*f),
1368            RuntimeValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
1369            RuntimeValue::Text(s) => s.as_str().to_string(),
1370            RuntimeValue::Char(c) => c.to_string(),
1371            RuntimeValue::List(items) => {
1372                let items = items.borrow();
1373                let parts: Vec<String> =
1374                    items.to_values().iter().map(|v| v.to_display_string()).collect();
1375                format!("[{}]", parts.join(", "))
1376            }
1377            RuntimeValue::Tuple(items) => {
1378                let parts: Vec<String> = items.iter().map(|v| v.to_display_string()).collect();
1379                format!("({})", parts.join(", "))
1380            }
1381            RuntimeValue::Set(items) => {
1382                let items = items.borrow();
1383                let parts: Vec<String> = items.iter().map(|v| v.to_display_string()).collect();
1384                format!("{{{}}}", parts.join(", "))
1385            }
1386            RuntimeValue::Map(m) => {
1387                let m = m.borrow();
1388                let pairs: Vec<String> = m.iter()
1389                    .map(|(k, v)| format!("{}: {}", k.to_display_string(), v.to_display_string()))
1390                    .collect();
1391                format!("{{{}}}", pairs.join(", "))
1392            }
1393            RuntimeValue::Struct(s) => {
1394                if s.fields.is_empty() {
1395                    s.type_name.clone()
1396                } else {
1397                    // `fields` is a `HashMap` (random iteration order), so sort by field NAME to make
1398                    // the display DETERMINISTIC — otherwise `TypeName { … }` order varies per run and
1399                    // can't be a byte-identical target for the VM/AOT tiers.
1400                    let mut field_strs: Vec<(&str, String)> = s
1401                        .fields
1402                        .iter()
1403                        .map(|(k, v)| (k.as_str(), v.to_display_string()))
1404                        .collect();
1405                    field_strs.sort_by(|a, b| a.0.cmp(b.0));
1406                    let joined: Vec<String> = field_strs.iter().map(|(k, v)| format!("{k}: {v}")).collect();
1407                    format!("{} {{ {} }}", s.type_name, joined.join(", "))
1408                }
1409            }
1410            RuntimeValue::Inductive(ind) => {
1411                if ind.args.is_empty() {
1412                    ind.constructor.clone()
1413                } else {
1414                    let arg_strs: Vec<String> = ind.args
1415                        .iter()
1416                        .map(|v| v.to_display_string())
1417                        .collect();
1418                    format!("{}({})", ind.constructor, arg_strs.join(", "))
1419                }
1420            }
1421            RuntimeValue::Function(_) => "<closure>".to_string(),
1422            RuntimeValue::Chan(_) => "<channel>".to_string(),
1423            RuntimeValue::TaskHandle(_) => "<task>".to_string(),
1424            RuntimeValue::Peer(topic) => format!("<peer {topic}>"),
1425            RuntimeValue::Crdt(c) => c.borrow().render(),
1426            RuntimeValue::Nothing => "nothing".to_string(),
1427            RuntimeValue::Duration(nanos) => {
1428                // Format durations nicely based on magnitude
1429                let abs_nanos = nanos.unsigned_abs();
1430                let sign = if *nanos < 0 { "-" } else { "" };
1431                if abs_nanos >= 3_600_000_000_000 {
1432                    // Hours
1433                    format!("{}{}h", sign, abs_nanos / 3_600_000_000_000)
1434                } else if abs_nanos >= 60_000_000_000 {
1435                    // Minutes
1436                    format!("{}{}min", sign, abs_nanos / 60_000_000_000)
1437                } else if abs_nanos >= 1_000_000_000 {
1438                    // Seconds
1439                    format!("{}{}s", sign, abs_nanos / 1_000_000_000)
1440                } else if abs_nanos >= 1_000_000 {
1441                    // Milliseconds
1442                    format!("{}{}ms", sign, abs_nanos / 1_000_000)
1443                } else if abs_nanos >= 1_000 {
1444                    // Microseconds
1445                    format!("{}{}μs", sign, abs_nanos / 1_000)
1446                } else {
1447                    // Nanoseconds
1448                    format!("{}{}ns", sign, abs_nanos)
1449                }
1450            }
1451            RuntimeValue::Date(days) => {
1452                // Convert days since epoch to YYYY-MM-DD format
1453                // Using Howard Hinnant's algorithm
1454                let z = *days as i64 + 719468; // shift epoch
1455                let era = if z >= 0 { z } else { z - 146096 } / 146097;
1456                let doe = z - era * 146097;
1457                let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
1458                let y = yoe + era * 400;
1459                let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1460                let mp = (5 * doy + 2) / 153;
1461                let d = doy - (153 * mp + 2) / 5 + 1;
1462                let m = mp + if mp < 10 { 3 } else { -9 };
1463                let year = y + if m <= 2 { 1 } else { 0 };
1464                format!("{:04}-{:02}-{:02}", year, m, d)
1465            }
1466            RuntimeValue::Moment(nanos) => {
1467                // Convert nanoseconds since epoch to ISO-8601-like datetime.
1468                // Use floored (Euclidean) division so a pre-epoch (negative)
1469                // Moment yields the correct date and a 0..86399 time-of-day,
1470                // not a negative hour/minute.
1471                let total_seconds = nanos.div_euclid(1_000_000_000);
1472                let days = total_seconds.div_euclid(86400) as i32;
1473                let day_seconds = total_seconds.rem_euclid(86400);
1474                let hours = day_seconds / 3600;
1475                let minutes = (day_seconds % 3600) / 60;
1476
1477                // Convert days since epoch to YYYY-MM-DD using Howard Hinnant's algorithm
1478                let z = days as i64 + 719468;
1479                let era = if z >= 0 { z } else { z - 146096 } / 146097;
1480                let doe = z - era * 146097;
1481                let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
1482                let y = yoe + era * 400;
1483                let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1484                let mp = (5 * doy + 2) / 153;
1485                let d = doy - (153 * mp + 2) / 5 + 1;
1486                let m = mp + if mp < 10 { 3 } else { -9 };
1487                let year = y + if m <= 2 { 1 } else { 0 };
1488
1489                format!("{:04}-{:02}-{:02} {:02}:{:02}", year, m, d, hours, minutes)
1490            }
1491            RuntimeValue::Span { months, days } => {
1492                // Format span with years, months, and days
1493                let mut parts = Vec::new();
1494
1495                // Extract years from months
1496                let years = *months / 12;
1497                let remaining_months = *months % 12;
1498
1499                if years != 0 {
1500                    parts.push(if years.abs() == 1 {
1501                        format!("{} year", years)
1502                    } else {
1503                        format!("{} years", years)
1504                    });
1505                }
1506
1507                if remaining_months != 0 {
1508                    parts.push(if remaining_months.abs() == 1 {
1509                        format!("{} month", remaining_months)
1510                    } else {
1511                        format!("{} months", remaining_months)
1512                    });
1513                }
1514
1515                if *days != 0 || parts.is_empty() {
1516                    parts.push(if days.abs() == 1 {
1517                        format!("{} day", days)
1518                    } else {
1519                        format!("{} days", days)
1520                    });
1521                }
1522
1523                parts.join(" and ")
1524            }
1525            RuntimeValue::Time(nanos) => {
1526                // The wall-clock time-of-day, lossless to the nanosecond (HH:MM:SS[.frac]).
1527                logicaffeine_base::temporal::format_time_of_day(*nanos)
1528            }
1529        }
1530    }
1531}
1532
1533/// Control flow signals returned from statement execution.
1534///
1535/// These signals allow the interpreter to handle early exits from blocks,
1536/// function returns, and loop breaks without exceptions.
1537pub enum ControlFlow {
1538    /// Continue normal execution to the next statement.
1539    Continue,
1540    /// Return from the current function with a value.
1541    Return(RuntimeValue),
1542    /// Break out of the current loop.
1543    Break,
1544}
1545
1546/// Stored function definition for user-defined functions.
1547///
1548/// Captures the parameter list, body statements, and optional return type
1549/// for later invocation when the function is called.
1550#[derive(Clone)]
1551pub struct FunctionDef<'a> {
1552    /// Parameter names paired with their type expressions.
1553    pub params: Vec<(Symbol, &'a TypeExpr<'a>)>,
1554    /// Statements comprising the function body.
1555    pub body: Block<'a>,
1556    /// Optional declared return type.
1557    pub return_type: Option<&'a TypeExpr<'a>>,
1558}
1559
1560/// Tree-walking interpreter for LOGOS programs.
1561///
1562/// Phase 55: Now async with optional VFS for file operations.
1563/// Phase 102: Kernel context for inductive type support.
1564/// Flat environment with O(1) lookup and undo-log scoping.
1565/// LEXICALLY scoped environment.
1566///
1567/// Main's top-level bindings are globals, visible (and assignable) everywhere.
1568/// Each function call swaps in a fresh `locals` frame — a callee sees its
1569/// parameters, its own bindings, and the globals, but NEVER its caller's
1570/// locals. Block scopes (If/While/Repeat bodies, Zone, Inspect arms) are
1571/// undo-logged: `define`s inside a block are reverted when it ends, while
1572/// `assign`s persist (mutation is not binding).
1573struct Environment {
1574    /// Main TOP-LEVEL bindings — the program's globals, visible everywhere.
1575    globals: HashMap<Symbol, RuntimeValue>,
1576    /// Main BLOCK-scoped bindings (Let inside an If/While/Repeat at Main
1577    /// level). Lexically NOT visible to called functions.
1578    main_block: HashMap<Symbol, RuntimeValue>,
1579    /// The current function frame's bindings.
1580    locals: HashMap<Symbol, RuntimeValue>,
1581    save_stack: Vec<Vec<(Symbol, Option<RuntimeValue>)>>,
1582    // Shelved (locals, save_stack) of each caller. The save stack is shelved
1583    // WITH the locals so a callee's defines can never be recorded into a
1584    // caller's undo frame.
1585    frame_stack: Vec<(HashMap<Symbol, RuntimeValue>, Vec<Vec<(Symbol, Option<RuntimeValue>)>>)>,
1586}
1587
1588impl Environment {
1589    fn new() -> Self {
1590        Environment {
1591            globals: HashMap::new(),
1592            main_block: HashMap::new(),
1593            locals: HashMap::new(),
1594            save_stack: Vec::new(),
1595            frame_stack: Vec::new(),
1596        }
1597    }
1598
1599    fn in_function(&self) -> bool {
1600        !self.frame_stack.is_empty()
1601    }
1602
1603    /// The map `define` writes to in the current context: function locals, or
1604    /// at Main level the block map (inside a block) vs the globals (at root).
1605    fn define_map(&mut self) -> &mut HashMap<Symbol, RuntimeValue> {
1606        if !self.frame_stack.is_empty() {
1607            &mut self.locals
1608        } else if !self.save_stack.is_empty() {
1609            &mut self.main_block
1610        } else {
1611            &mut self.globals
1612        }
1613    }
1614
1615    /// Enter a function frame: the caller's locals and undo log are shelved;
1616    /// the callee starts with neither (the lexical barrier).
1617    fn push_frame(&mut self) {
1618        self.frame_stack.push((
1619            std::mem::take(&mut self.locals),
1620            std::mem::take(&mut self.save_stack),
1621        ));
1622    }
1623
1624    /// Leave a function frame, restoring the caller's locals and undo log.
1625    fn pop_frame(&mut self) {
1626        let (locals, saves) = self.frame_stack.pop().unwrap_or_default();
1627        self.locals = locals;
1628        self.save_stack = saves;
1629    }
1630
1631    fn push_scope(&mut self) {
1632        self.save_stack.push(Vec::new());
1633    }
1634
1635    fn pop_scope(&mut self) {
1636        if let Some(saves) = self.save_stack.pop() {
1637            let map = if !self.frame_stack.is_empty() {
1638                &mut self.locals
1639            } else {
1640                // Main level: block-scoped defines live in main_block (a
1641                // define at Main ROOT records no undo — save_stack was empty).
1642                &mut self.main_block
1643            };
1644            for (sym, old_val) in saves.into_iter().rev() {
1645                match old_val {
1646                    Some(val) => { map.insert(sym, val); }
1647                    None => { map.remove(&sym); }
1648                }
1649            }
1650        }
1651    }
1652
1653    fn define(&mut self, name: Symbol, value: RuntimeValue) {
1654        let map = self.define_map();
1655        let old = map.insert(name, value);
1656        if let Some(frame) = self.save_stack.last_mut() {
1657            frame.push((name, old));
1658        }
1659    }
1660
1661    fn lookup(&self, name: Symbol) -> Option<&RuntimeValue> {
1662        if self.in_function() {
1663            self.locals.get(&name).or_else(|| self.globals.get(&name))
1664        } else {
1665            self.main_block.get(&name).or_else(|| self.globals.get(&name))
1666        }
1667    }
1668
1669    fn assign(&mut self, name: Symbol, value: RuntimeValue) -> bool {
1670        if self.in_function() {
1671            if self.locals.contains_key(&name) {
1672                self.locals.insert(name, value);
1673                return true;
1674            }
1675        } else if self.main_block.contains_key(&name) {
1676            self.main_block.insert(name, value);
1677            return true;
1678        }
1679        if self.globals.contains_key(&name) {
1680            self.globals.insert(name, value);
1681            true
1682        } else {
1683            false
1684        }
1685    }
1686}
1687
1688/// Side-table entry storing a closure body AST reference.
1689/// The index into the `closure_bodies` Vec on the interpreter is stored
1690/// in `ClosureValue::body_index`.
1691#[derive(Clone)]
1692pub enum ClosureBodyRef<'a> {
1693    Expression(&'a Expr<'a>),
1694    Block(Block<'a>),
1695}
1696
1697/// `Send redundant` FEC parameters: split into `REDUNDANT_K` data shards plus
1698/// `REDUNDANT_N − REDUNDANT_K` parity shards, so a receiver reconstructs from any
1699/// `REDUNDANT_K` and tolerates losing up to `REDUNDANT_N − REDUNDANT_K` of the `REDUNDANT_N`
1700/// (here 2 of 6 — a 33% loss budget at 1.5× bandwidth).
1701const REDUNDANT_K: usize = 4;
1702const REDUNDANT_N: usize = 6;
1703
1704pub struct Interpreter<'a> {
1705    /// Shared, mostly-immutable context — interner, function/struct tables,
1706    /// platform handles, pre-interned builtin symbols. Held directly for the
1707    /// single-task case; wrapped in `Rc<SharedCtx>` and shared across per-task
1708    /// `Interpreter` instances once the scheduler spawns concurrent tasks.
1709    ctx: SharedCtx<'a>,
1710    /// Per-task execution state — owned per task so the cooperative scheduler can
1711    /// run multiple task continuations without aliasing the interpreter.
1712    task: TaskState,
1713    /// The program's output lines. Public API consumed by `ui_bridge`.
1714    pub output: Vec<String>,
1715    /// Set when this interpreter is a scheduled concurrent task: the side-channel
1716    /// to the scheduler. `None` for ordinary single-task execution.
1717    yield_state: Option<crate::concurrency::bridge::Yield<'a>>,
1718    /// All peer-messaging state — the relay handle, this node's inbox topic, the received-message
1719    /// buffer, the wire schema caches, and the FEC shard buffer — lifted into one shared
1720    /// [`crate::concurrency::net_inbox::NetInbox`] so the bytecode VM's task driver owns the SAME
1721    /// inbox and networking runs byte-identically on both tiers (no tier silently differs).
1722    netbox: crate::concurrency::net_inbox::NetInbox,
1723}
1724
1725/// The shared interpreter context: function definitions, type metadata, platform
1726/// handles, and pre-interned builtin symbols. Immutable-after-setup, so multiple
1727/// per-task [`Interpreter`]s can share one `Rc<SharedCtx>` while each owns its own
1728/// [`TaskState`] — the basis of the tree-walker's re-entrancy for concurrency.
1729#[derive(Clone)]
1730struct SharedCtx<'a> {
1731    interner: &'a Interner,
1732    functions: HashMap<Symbol, FunctionDef<'a>>,
1733    struct_defs: HashMap<Symbol, Vec<(Symbol, Symbol, bool)>>,
1734    /// Enum type → its constructor names in declaration order. Feeds the wire type
1735    /// registry so `Send shared` elides enum type/constructor names (T_INDUCTIVE_TID),
1736    /// the enum analog of struct name elision.
1737    enum_defs: HashMap<Symbol, Vec<Symbol>>,
1738    vfs: Option<Arc<dyn Vfs>>,
1739    kernel_ctx: Option<Arc<crate::kernel::Context>>,
1740    policy_registry: Option<PolicyRegistry>,
1741    output_callback: Option<OutputCallback>,
1742    /// Side-table for closure body AST references.
1743    /// Indexed by `ClosureValue::body_index`.
1744    closure_bodies: Vec<ClosureBodyRef<'a>>,
1745    // Pre-interned builtin function symbols for O(1) dispatch
1746    sym_show: Option<Symbol>,
1747    sym_length: Option<Symbol>,
1748    sym_format: Option<Symbol>,
1749    sym_parse_int: Option<Symbol>,
1750    sym_parse_float: Option<Symbol>,
1751    sym_abs: Option<Symbol>,
1752    sym_sqrt: Option<Symbol>,
1753    sym_min: Option<Symbol>,
1754    sym_max: Option<Symbol>,
1755    sym_floor: Option<Symbol>,
1756    sym_ceil: Option<Symbol>,
1757    sym_round: Option<Symbol>,
1758    sym_pow: Option<Symbol>,
1759    sym_copy: Option<Symbol>,
1760    sym_chr: Option<Symbol>,
1761    sym_count_ones: Option<Symbol>,
1762    sym_args: Option<Symbol>,
1763    /// Program arguments for the `args()` system native — full argv, index 0 is
1764    /// the program name (mirrors the compiled binary's `env::args()`).
1765    program_args: Vec<String>,
1766}
1767
1768/// The per-task execution state the cooperative scheduler owns for each task.
1769/// Splitting it out of [`Interpreter`] lets multiple task continuations coexist —
1770/// each with its own `&mut TaskState` over one shared interpreter context — which
1771/// is what makes the tree-walker re-entrant for concurrency.
1772struct TaskState {
1773    /// Variable bindings / scopes for this task.
1774    env: Environment,
1775    /// Live LOGOS call depth, bounded by `semantics::MAX_CALL_DEPTH`.
1776    call_depth: usize,
1777    /// The user function whose body the SYNC path is currently executing. A
1778    /// `Return self(args)` (or the `Set/Let x to self(args); Return x` pair) of
1779    /// THIS function is a self-tail-call: `call_function_sync` reassigns the
1780    /// parameters and loops to the body's start instead of recursing, so tail
1781    /// recursion runs in constant stack — matching the VM and the AOT TCE.
1782    tco_fn_sync: Option<Symbol>,
1783    /// Set by a recognized self-tail-call: the already-evaluated arguments for
1784    /// the next loop iteration, consumed by `call_function_sync`.
1785    pending_tail_call: Option<Vec<RuntimeValue>>,
1786    /// `Repeat` (for-each) nesting on the SYNC path within the current function
1787    /// body. A `Repeat` owns a live iterator, so — exactly like the VM's
1788    /// `is_repeat` guard — a self-tail-call detected inside one stays an ordinary
1789    /// recursive call, keeping the two engines bit-identical. Reset at call boundaries.
1790    repeat_depth_sync: usize,
1791    /// `tco_fn_sync` for the ASYNC execution path; same constant-stack TCO semantics.
1792    tco_fn_async: Option<Symbol>,
1793    /// `pending_tail_call` for the ASYNC path.
1794    pending_tail_call_async: Option<Vec<RuntimeValue>>,
1795    /// `repeat_depth_sync` for the ASYNC path.
1796    repeat_depth_async: usize,
1797}
1798
1799impl TaskState {
1800    fn new() -> Self {
1801        TaskState {
1802            env: Environment::new(),
1803            call_depth: 0,
1804            tco_fn_sync: None,
1805            pending_tail_call: None,
1806            repeat_depth_sync: 0,
1807            tco_fn_async: None,
1808            pending_tail_call_async: None,
1809            repeat_depth_async: 0,
1810        }
1811    }
1812}
1813
1814impl<'a> Interpreter<'a> {
1815    pub fn new(interner: &'a Interner) -> Self {
1816        Interpreter {
1817            ctx: SharedCtx {
1818                interner,
1819                functions: HashMap::new(),
1820                struct_defs: HashMap::new(),
1821                enum_defs: HashMap::new(),
1822                vfs: None,
1823                kernel_ctx: None,
1824                policy_registry: None,
1825                output_callback: None,
1826                closure_bodies: Vec::new(),
1827                sym_show: interner.lookup("show"),
1828                sym_length: interner.lookup("length"),
1829                sym_format: interner.lookup("format"),
1830                sym_parse_int: interner.lookup("parseInt"),
1831                sym_parse_float: interner.lookup("parseFloat"),
1832                sym_abs: interner.lookup("abs"),
1833                sym_sqrt: interner.lookup("sqrt"),
1834                sym_min: interner.lookup("min"),
1835                sym_max: interner.lookup("max"),
1836                sym_floor: interner.lookup("floor"),
1837                sym_ceil: interner.lookup("ceil"),
1838                sym_round: interner.lookup("round"),
1839                sym_pow: interner.lookup("pow"),
1840                sym_copy: interner.lookup("copy"),
1841                sym_chr: interner.lookup("chr"),
1842                sym_count_ones: interner.lookup("count_ones"),
1843                sym_args: interner.lookup("args"),
1844                program_args: Vec::new(),
1845            },
1846            task: TaskState::new(),
1847            output: Vec::new(),
1848            yield_state: None,
1849            netbox: crate::concurrency::net_inbox::NetInbox::new(),
1850        }
1851    }
1852
1853    /// Supply the program arguments read by the `args()` system native. The
1854    /// vector is the full argv (index 0 is the program name), matching the
1855    /// compiled binary's `env::args()`.
1856    pub fn with_program_args(mut self, args: Vec<String>) -> Self {
1857        self.ctx.program_args = args;
1858        self
1859    }
1860
1861    /// Phase 55: Set the VFS for file operations.
1862    pub fn with_vfs(mut self, vfs: Arc<dyn Vfs>) -> Self {
1863        self.ctx.vfs = Some(vfs);
1864        self
1865    }
1866
1867    /// Phase 102: Set the kernel context for inductive type support.
1868    ///
1869    /// When set, the interpreter can query the kernel for inductive types
1870    /// and constructors, enabling unified type system.
1871    pub fn with_kernel(mut self, ctx: Arc<crate::kernel::Context>) -> Self {
1872        self.ctx.kernel_ctx = Some(ctx);
1873        self
1874    }
1875
1876    /// Set the policy registry for security checks.
1877    pub fn with_policies(mut self, registry: PolicyRegistry) -> Self {
1878        self.ctx.policy_registry = Some(registry);
1879        self
1880    }
1881
1882    /// Populate struct_defs from a TypeRegistry (DiscoveryPass results).
1883    /// This allows the interpreter to initialize default field values for
1884    /// structs created with `new Point` (no explicit fields).
1885    pub fn with_type_registry(mut self, registry: &crate::analysis::TypeRegistry) -> Self {
1886        use crate::analysis::registry::{TypeDef, FieldType};
1887        for (name_sym, type_def) in registry.iter_types() {
1888            if let TypeDef::Struct { fields, .. } = type_def {
1889                let field_defs: Vec<(Symbol, Symbol, bool)> = fields.iter().map(|f| {
1890                    let type_sym = match &f.ty {
1891                        FieldType::Primitive(s) | FieldType::Named(s) | FieldType::TypeParam(s) => *s,
1892                        FieldType::Generic { base, .. } => *base,
1893                    };
1894                    (f.name, type_sym, f.is_public)
1895                }).collect();
1896                self.ctx.struct_defs.insert(*name_sym, field_defs);
1897            } else if let TypeDef::Enum { variants, .. } = type_def {
1898                // Constructor names in declaration order — the order is the wire's ctor
1899                // index, so both peers (deriving from the same program) agree.
1900                let ctors: Vec<Symbol> = variants.iter().map(|v| v.name).collect();
1901                self.ctx.enum_defs.insert(*name_sym, ctors);
1902            }
1903        }
1904        self
1905    }
1906
1907    /// Set a callback for streaming output.
1908    /// The callback is called each time `Show` executes, with the output line.
1909    pub fn with_output_callback(mut self, callback: OutputCallback) -> Self {
1910        self.ctx.output_callback = Some(callback);
1911        self
1912    }
1913
1914    /// Install the scheduler side-channel, marking this interpreter as a scheduled
1915    /// concurrent task (used by the scheduler-driven run path).
1916    pub(crate) fn install_yield_state(&mut self, ys: crate::concurrency::bridge::Yield<'a>) {
1917        self.yield_state = Some(ys);
1918    }
1919
1920    /// Internal helper to emit output (calls callback if set, always adds to output vec)
1921    fn emit_output(&mut self, line: String) {
1922        if let Some(ref callback) = self.ctx.output_callback {
1923            (callback.borrow_mut())(line.clone());
1924        }
1925        self.output.push(line);
1926    }
1927
1928    /// Phase 102: Check if a name is a kernel inductive type.
1929    pub fn is_kernel_inductive(&self, name: &str) -> bool {
1930        self.ctx.kernel_ctx
1931            .as_ref()
1932            .map(|ctx| ctx.is_inductive(name))
1933            .unwrap_or(false)
1934    }
1935
1936    /// Phase 102: Get constructors for a kernel inductive type.
1937    ///
1938    /// Returns a vector of (constructor_name, arity) pairs.
1939    pub fn get_kernel_constructors(&self, name: &str) -> Vec<(String, usize)> {
1940        self.ctx.kernel_ctx
1941            .as_ref()
1942            .map(|ctx| {
1943                ctx.get_constructors(name)
1944                    .iter()
1945                    .map(|(ctor_name, ty)| {
1946                        // Count Pi types to determine arity
1947                        let arity = count_pi_args(ty);
1948                        (ctor_name.to_string(), arity)
1949                    })
1950                    .collect()
1951            })
1952            .unwrap_or_default()
1953    }
1954
1955    /// Execute a program (list of statements).
1956    /// Phase 55: Now async for VFS operations.
1957    pub async fn run(&mut self, stmts: &[Stmt<'a>]) -> Result<(), String> {
1958        // A program begins with no ambient exchange rates in scope — the same clean slate an
1959        // AOT-compiled binary gets from a fresh process. Conversion reads what the program installs.
1960        logicaffeine_base::money::clear_ambient_rates();
1961        for stmt in stmts {
1962            match self.execute_stmt(stmt).await? {
1963                ControlFlow::Return(_) => break,
1964                ControlFlow::Break => break,
1965                ControlFlow::Continue => {}
1966            }
1967        }
1968        Ok(())
1969    }
1970
1971    /// Activate the PNP one-time-pad session for a `Connect`/`Listen` `with pad "<path>" as <role>`
1972    /// clause: read the pad file, build the quality-gated pool, and install the directional session on
1973    /// the channel so every subsequent `Send`/receive on this thread is sealed. Fail-closed on any
1974    /// error (unreadable or non-random pad) — the caller propagates it as a program error, never
1975    /// proceeding to send plaintext.
1976    #[cfg(not(target_arch = "wasm32"))]
1977    async fn activate_pnp_session(&mut self, bind: &crate::ast::SecurePad<'a>) -> Result<(), String> {
1978        let path = self.evaluate_expr(bind.pad).await?.to_display_string();
1979        let bytes = std::fs::read(&path)
1980            .map_err(|e| format!("one-time pad '{path}' could not be read: {e}"))?;
1981        let pool = crate::concurrency::pnp::PadPool::shared(bytes)
1982            .map_err(|e| format!("one-time pad '{path}' rejected (not truly random / too small): {e:?}"))?;
1983        let role = match bind.role {
1984            crate::ast::SecureRole::Initiator => crate::concurrency::pnp::Role::Initiator,
1985            crate::ast::SecureRole::Responder => crate::concurrency::pnp::Role::Responder,
1986        };
1987        let session: std::rc::Rc<dyn crate::concurrency::channel::ActiveSession> =
1988            std::rc::Rc::new(pool.session(role));
1989        crate::concurrency::channel::install_session(Some(session));
1990        Ok(())
1991    }
1992
1993    /// On wasm the pad is provisioned through the VFS handle rather than host files (a future wiring);
1994    /// the clause is accepted but installs no session, matching the offline single-node tiers.
1995    #[cfg(target_arch = "wasm32")]
1996    async fn activate_pnp_session(&mut self, _bind: &crate::ast::SecurePad<'a>) -> Result<(), String> {
1997        Ok(())
1998    }
1999
2000    /// Execute a single statement.
2001    /// Phase 55: Now async for VFS operations.
2002    #[async_recursion(?Send)]
2003    async fn execute_stmt(&mut self, stmt: &Stmt<'a>) -> Result<ControlFlow, String> {
2004        match stmt {
2005            Stmt::Let { var, value, .. } => {
2006                let val = self.evaluate_expr(value).await?;
2007                self.define(*var, val);
2008                Ok(ControlFlow::Continue)
2009            }
2010
2011            Stmt::Set { target, value } => {
2012                let val = self.evaluate_expr(value).await?;
2013                self.assign(*target, val)?;
2014                Ok(ControlFlow::Continue)
2015            }
2016
2017            Stmt::Call { function, args } => {
2018                self.call_function(*function, args).await?;
2019                Ok(ControlFlow::Continue)
2020            }
2021
2022            Stmt::If { cond, then_block, else_block } => {
2023                let condition = self.evaluate_expr(cond).await?;
2024                if condition.is_truthy() {
2025                    let flow = self.execute_block(then_block).await?;
2026                    if !matches!(flow, ControlFlow::Continue) {
2027                        return Ok(flow);
2028                    }
2029                } else if let Some(else_stmts) = else_block {
2030                    let flow = self.execute_block(else_stmts).await?;
2031                    if !matches!(flow, ControlFlow::Continue) {
2032                        return Ok(flow);
2033                    }
2034                }
2035                Ok(ControlFlow::Continue)
2036            }
2037
2038            Stmt::While { cond, body, .. } => {
2039                loop {
2040                    let condition = self.evaluate_expr(cond).await?;
2041                    if !condition.is_truthy() {
2042                        break;
2043                    }
2044                    match self.execute_block(body).await? {
2045                        ControlFlow::Break => break,
2046                        ControlFlow::Return(v) => return Ok(ControlFlow::Return(v)),
2047                        ControlFlow::Continue => {}
2048                    }
2049                }
2050                Ok(ControlFlow::Continue)
2051            }
2052
2053            Stmt::Repeat { pattern, iterable, body } => {
2054                use crate::ast::stmt::Pattern;
2055
2056                let iter_val = self.evaluate_expr(iterable).await?;
2057                let items = crate::semantics::collections::iteration_snapshot(&iter_val)?;
2058
2059                self.push_scope();
2060                // Suppress TCO inside a `Repeat` (live iterator) — see SYNC twin.
2061                self.task.repeat_depth_async += 1;
2062                for item in items {
2063                    // Bind variables according to pattern
2064                    match pattern {
2065                        Pattern::Identifier(sym) => {
2066                            self.define(*sym, item);
2067                        }
2068                        Pattern::Tuple(syms) => {
2069                            if let RuntimeValue::Tuple(ref tuple_vals) = item {
2070                                if syms.len() != tuple_vals.len() {
2071                                    self.task.repeat_depth_async -= 1;
2072                                    return Err(format!(
2073                                        "Cannot bind a {}-tuple to {} names",
2074                                        tuple_vals.len(),
2075                                        syms.len()
2076                                    ));
2077                                }
2078                                for (sym, val) in syms.iter().zip(tuple_vals.iter()) {
2079                                    self.define(*sym, val.clone());
2080                                }
2081                            } else {
2082                                self.task.repeat_depth_async -= 1;
2083                                return Err(format!("Expected tuple for pattern, got {}", item.type_name()));
2084                            }
2085                        }
2086                    }
2087
2088                    match self.execute_block(body).await? {
2089                        ControlFlow::Break => break,
2090                        ControlFlow::Return(v) => {
2091                            self.task.repeat_depth_async -= 1;
2092                            self.pop_scope();
2093                            return Ok(ControlFlow::Return(v));
2094                        }
2095                        ControlFlow::Continue => {}
2096                    }
2097                }
2098                self.task.repeat_depth_async -= 1;
2099                self.pop_scope();
2100                Ok(ControlFlow::Continue)
2101            }
2102
2103            Stmt::Return { value } => {
2104                // Direct self-tail-call → loop-back in `call_function` (see the
2105                // SYNC twin for the full rationale).
2106                if let Some(expr) = value {
2107                    if let Some(call_args) = self.self_tail_call_args_async(*expr) {
2108                        let mut vals = Vec::with_capacity(call_args.len());
2109                        for a in call_args {
2110                            vals.push(self.evaluate_expr(a).await?);
2111                        }
2112                        self.task.pending_tail_call_async = Some(vals);
2113                        return Ok(ControlFlow::Return(RuntimeValue::Nothing));
2114                    }
2115                }
2116                let ret_val = match value {
2117                    Some(expr) => self.evaluate_expr(expr).await?,
2118                    None => RuntimeValue::Nothing,
2119                };
2120                Ok(ControlFlow::Return(ret_val))
2121            }
2122
2123            Stmt::Break => Ok(ControlFlow::Break),
2124
2125            Stmt::FunctionDef { name, params, body, return_type, .. } => {
2126                let func = FunctionDef {
2127                    params: params.clone(),
2128                    body: *body,
2129                    return_type: *return_type,
2130                };
2131                self.ctx.functions.insert(*name, func);
2132                Ok(ControlFlow::Continue)
2133            }
2134
2135            Stmt::StructDef { name, fields, .. } => {
2136                self.ctx.struct_defs.insert(*name, fields.clone());
2137                Ok(ControlFlow::Continue)
2138            }
2139
2140            Stmt::SetField { object, field, value } => {
2141                let new_val = self.evaluate_expr(value).await?;
2142                if let Expr::Identifier(obj_sym) = object {
2143                    let mut obj_val = self.lookup(*obj_sym)?.clone();
2144                    if let RuntimeValue::Struct(ref mut s) = obj_val {
2145                        let field_name = self.ctx.interner.resolve(*field).to_string();
2146                        s.fields.insert(field_name, new_val);
2147                        self.assign(*obj_sym, obj_val)?;
2148                    } else {
2149                        return Err(format!("Cannot set field on non-struct value"));
2150                    }
2151                } else {
2152                    return Err("SetField target must be an identifier".to_string());
2153                }
2154                Ok(ControlFlow::Continue)
2155            }
2156
2157            Stmt::Push { value, collection } => {
2158                let val = self.evaluate_expr(value).await?;
2159                if let Expr::Identifier(coll_sym) = collection {
2160                    self.ensure_collection_owned(*coll_sym);
2161                    let coll_val = self.lookup(*coll_sym)?;
2162                    crate::semantics::collections::list_push(&coll_val, val)?;
2163                } else if let Expr::FieldAccess { object, field } = collection {
2164                    if let Expr::Identifier(obj_sym) = *object {
2165                        let obj_val = self.lookup(*obj_sym)?;
2166                        let field_name = self.ctx.interner.resolve(*field);
2167                        crate::semantics::collections::push_to_struct_field(&obj_val, field_name, val)?;
2168                    } else {
2169                        return Err("Push to nested field access not supported".to_string());
2170                    }
2171                } else {
2172                    // Any place expression is an l-value: `Push 5 to item i of
2173                    // grid`. Collections are shared handles, so pushing through
2174                    // the evaluated handle mutates in place — the same aliasing
2175                    // model as `Add`/`Remove` below.
2176                    let coll_val = self.evaluate_expr(collection).await?;
2177                    crate::semantics::collections::list_push(&coll_val, val)?;
2178                }
2179                Ok(ControlFlow::Continue)
2180            }
2181
2182            Stmt::Pop { collection, into } => {
2183                if let Expr::Identifier(coll_sym) = collection {
2184                    self.ensure_collection_owned(*coll_sym);
2185                    let coll_val = self.lookup(*coll_sym)?;
2186                    let popped = crate::semantics::collections::list_pop(&coll_val)?;
2187                    if let Some(into_var) = into {
2188                        self.define(*into_var, popped);
2189                    }
2190                } else {
2191                    return Err("Pop collection must be an identifier".to_string());
2192                }
2193                Ok(ControlFlow::Continue)
2194            }
2195
2196            Stmt::Add { value, collection } => {
2197                let val = self.evaluate_expr(value).await?;
2198                if let Expr::Identifier(coll_sym) = collection {
2199                    self.ensure_collection_owned(*coll_sym);
2200                }
2201                // The collection may be a bare variable OR a (CRDT/Set) struct field —
2202                // `Add "Alice" to p's guests`. Field reads are shallow `Rc` clones, so
2203                // mutating the resolved collection updates the value stored in the struct.
2204                let coll_val = self.evaluate_expr(collection).await?;
2205                crate::semantics::collections::set_add(&coll_val, val)?;
2206                Ok(ControlFlow::Continue)
2207            }
2208
2209            Stmt::Remove { value, collection } => {
2210                let val = self.evaluate_expr(value).await?;
2211                if let Expr::Identifier(coll_sym) = collection {
2212                    self.ensure_collection_owned(*coll_sym);
2213                }
2214                let coll_val = self.evaluate_expr(collection).await?;
2215                crate::semantics::collections::remove_from(&coll_val, &val)?;
2216                Ok(ControlFlow::Continue)
2217            }
2218
2219            Stmt::SetIndex { collection, index, value } => {
2220                let idx_val = self.evaluate_expr(index).await?;
2221                let new_val = self.evaluate_expr(value).await?;
2222                if let Expr::Identifier(coll_sym) = collection {
2223                    // Struct field set via index syntax: `Set item "field" of structVar to v`.
2224                    // Mirrors the read side (`item "field" of struct`) so struct-field
2225                    // mutation round-trips through the decompiler's CMapSet rendering.
2226                    if let RuntimeValue::Text(field) = &idx_val {
2227                        let cur = self.lookup(*coll_sym)?.clone();
2228                        if let RuntimeValue::Struct(mut s) = cur {
2229                            s.fields.insert(field.to_string(), new_val);
2230                            self.assign(*coll_sym, RuntimeValue::Struct(s))?;
2231                            return Ok(ControlFlow::Continue);
2232                        }
2233                    }
2234                    self.ensure_collection_owned(*coll_sym);
2235                    let coll_val = self.lookup(*coll_sym)?;
2236                    crate::semantics::collections::index_set(&coll_val, &idx_val, new_val)?;
2237                } else {
2238                    // Any place expression is an l-value: `Set item j of
2239                    // (item i of grid) to v` writes the inner collection
2240                    // through its shared handle.
2241                    let coll_val = self.evaluate_expr(collection).await?;
2242                    crate::semantics::collections::index_set(&coll_val, &idx_val, new_val)?;
2243                }
2244                Ok(ControlFlow::Continue)
2245            }
2246
2247            Stmt::Splice { body } => {
2248                // Scope-TRANSPARENT by contract (see the AST doc): no block
2249                // scoping — the gensym'd desugar temporaries live in the
2250                // enclosing scope.
2251                for s in body.iter() {
2252                    let flow = self.execute_stmt(s).await?;
2253                    if !matches!(flow, ControlFlow::Continue) {
2254                        return Ok(flow);
2255                    }
2256                }
2257                Ok(ControlFlow::Continue)
2258            }
2259
2260            Stmt::Inspect { target, arms, .. } => {
2261                let target_val = self.evaluate_expr(target).await?;
2262                self.execute_inspect(&target_val, arms).await
2263            }
2264
2265            Stmt::Zone { name, body, .. } => {
2266                self.push_scope();
2267                self.define(*name, RuntimeValue::Nothing);
2268                let result = self.execute_block(body).await;
2269                self.pop_scope();
2270                result?;
2271                Ok(ControlFlow::Continue)
2272            }
2273
2274            Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
2275                // In WASM, execute sequentially (no threads)
2276                for task in tasks.iter() {
2277                    self.execute_stmt(task).await?;
2278                }
2279                Ok(ControlFlow::Continue)
2280            }
2281
2282            Stmt::Assert { .. } | Stmt::Trust { .. } => {
2283                Ok(ControlFlow::Continue)
2284            }
2285
2286            Stmt::RuntimeAssert { condition, .. } => {
2287                let val = self.evaluate_expr(condition).await?;
2288                if !val.is_truthy() {
2289                    return Err("Assertion failed".to_string());
2290                }
2291                Ok(ControlFlow::Continue)
2292            }
2293
2294            Stmt::Give { object, recipient } => {
2295                let obj_val = self.evaluate_expr(object).await?;
2296                if let Expr::Identifier(sym) = recipient {
2297                    self.call_function_with_values(*sym, vec![obj_val]).await?;
2298                }
2299                Ok(ControlFlow::Continue)
2300            }
2301
2302            Stmt::Show { object, recipient } => {
2303                let obj_val = self.evaluate_expr(object).await?;
2304                if let Expr::Identifier(sym) = recipient {
2305                    let name = self.ctx.interner.resolve(*sym);
2306                    if name == "show" {
2307                        self.emit_output(obj_val.to_display_string());
2308                    } else {
2309                        self.call_function_with_values(*sym, vec![obj_val]).await?;
2310                    }
2311                }
2312                Ok(ControlFlow::Continue)
2313            }
2314
2315            // Phase 55: VFS operations now supported
2316            Stmt::ReadFrom { var, source } => {
2317                let content = match source {
2318                    ReadSource::Console => {
2319                        // Console read not available in WASM interpreter
2320                        String::new()
2321                    }
2322                    ReadSource::File(path_expr) => {
2323                        let path = self.evaluate_expr(path_expr).await?.to_display_string();
2324                        match &self.ctx.vfs {
2325                            Some(vfs) => {
2326                                vfs.read_to_string(&path).await
2327                                    .map_err(|e| format!("Read error: {}", e))?
2328                            }
2329                            None => return Err("VFS not initialized. Use Interpreter::with_vfs()".to_string()),
2330                        }
2331                    }
2332                };
2333                self.define(*var, RuntimeValue::Text(Rc::new(content)));
2334                Ok(ControlFlow::Continue)
2335            }
2336
2337            Stmt::WriteFile { content, path } => {
2338                let content_val = self.evaluate_expr(content).await?.to_display_string();
2339                let path_val = self.evaluate_expr(path).await?.to_display_string();
2340                match &self.ctx.vfs {
2341                    Some(vfs) => {
2342                        vfs.write(&path_val, content_val.as_bytes()).await
2343                            .map_err(|e| format!("Write error: {}", e))?;
2344                    }
2345                    None => return Err("VFS not initialized. Use Interpreter::with_vfs()".to_string()),
2346                }
2347                Ok(ControlFlow::Continue)
2348            }
2349
2350            Stmt::Spawn { name, .. } => {
2351                self.define(*name, RuntimeValue::Nothing);
2352                Ok(ControlFlow::Continue)
2353            }
2354
2355            // `Send <message> to <peer>` — publish the message on the peer's inbox
2356            // topic over the relay, tagged with our own inbox as the sender so the
2357            // recipient's `Await … from us` can match it.
2358            Stmt::SendMessage { message, destination, compression, cached, unchecked, layout, shared, computed, indexed, deduped } => {
2359                use crate::concurrency::marshal::{
2360                    default_integrity, message_to_wire_best, message_to_wire_cached, message_to_wire_with,
2361                    with_compression_codec, with_dedup, with_integrity, with_numerics, with_structure,
2362                    with_struct_view, with_type_registry, WireCodec, WireGoal, WireIntegrity, WireNumerics,
2363                    WireSchemaCache, WireStructure,
2364                };
2365                use logicaffeine_language::ast::SendLayout;
2366                let dest = self.evaluate_expr(destination).await?;
2367                let topic = Self::peer_topic_of(&dest)?;
2368                let msg = self.evaluate_expr(message).await?;
2369                // `Send computed f` — COMPUTE-SHIPPING: lower a pure single-argument function
2370                // into the sandboxed generator so the COMPUTATION crosses the wire (as a
2371                // callable the receiver evaluates in its bounded sandbox), not data. The
2372                // lowering is the safety gate: only a total arithmetic expression over the
2373                // one argument lowers; anything else (I/O, calls, a block, >1 param) is
2374                // refused here, never shipped.
2375                let msg = if *computed {
2376                    match msg {
2377                        RuntimeValue::Function(c) if c.generated.is_some() => RuntimeValue::Function(c),
2378                        RuntimeValue::Function(c) => {
2379                            if c.param_names.len() != 1 {
2380                                return Err(
2381                                    "Send computed requires a single-argument pure function".to_string()
2382                                );
2383                            }
2384                            let expr = match self.ctx.closure_bodies.get(c.body_index) {
2385                                Some(ClosureBodyRef::Expression(e)) => *e,
2386                                _ => {
2387                                    return Err(
2388                                        "Send computed requires a pure expression-bodied function".to_string()
2389                                    )
2390                                }
2391                            };
2392                            match crate::concurrency::marshal::lower_expr_to_genexpr(expr, c.param_names[0]) {
2393                                Some(gen) => RuntimeValue::Function(Box::new(ClosureValue {
2394                                    body_index: usize::MAX,
2395                                    captured_env: HashMap::default(),
2396                                    param_names: c.param_names.clone(),
2397                                    generated: Some(std::rc::Rc::new(gen)),
2398                                })),
2399                                None => {
2400                                    return Err(
2401                                        "Send computed: the function is not a pure arithmetic computation over its argument"
2402                                            .to_string(),
2403                                    )
2404                                }
2405                            }
2406                        }
2407                        _ => return Err("Send computed requires a function value".to_string()),
2408                    }
2409                } else {
2410                    msg
2411                };
2412                // Owned so the schema-cache borrow below doesn't alias the inbox topic.
2413                let from = self.netbox.inbox.as_ref().map(|t| t.to_string()).unwrap_or_default();
2414                // Advertise our type-registry epoch (set before the first-contact handshake below), so a
2415                // same-program peer with a matching epoch negotiates type-id name elision.
2416                self.netbox.my_profile.registry_epoch = self.build_wire_type_registry().epoch();
2417                // The message is any language value, encoded faithfully for the wire. The
2418                // sender's modifiers are the knobs for their link: `fast|compact|packed`
2419                // is the size↔speed LAYOUT (fixed-memcpy / varint / group-varint);
2420                // `compressed [with <codec>]` shrinks the body (kept only if it helps);
2421                // `cached` references a once-sent struct schema by id; `unchecked` drops
2422                // the integrity checksum (latency↔safety). The wire is self-describing,
2423                // so any peer decodes it regardless of which knobs the sender turned.
2424                let integrity = if *unchecked { WireIntegrity::Raw } else { default_integrity() };
2425                let numerics = match layout {
2426                    Some(SendLayout::Fast) => WireNumerics::Fixed,
2427                    Some(SendLayout::Packed) => WireNumerics::GroupVarint,
2428                    // `smallest`/`best` lets the per-column menu pick each column's own
2429                    // form, so the numeric dial stays the varint baseline it builds on.
2430                    Some(SendLayout::Smallest) => WireNumerics::Varint,
2431                    // `redundant` adds FEC framing OVER the default encoding.
2432                    Some(SendLayout::Redundant) => WireNumerics::Varint,
2433                    Some(SendLayout::Compact) | None => WireNumerics::Varint,
2434                };
2435                // `smallest` turns on the per-column compression menu (delta / DoD /
2436                // frame-of-reference / RLE / dictionary, auto-selected, never worse than
2437                // varint); every other layout leaves structural analysis off.
2438                let structure = match layout {
2439                    Some(SendLayout::Smallest) => WireStructure::Auto,
2440                    _ => WireStructure::Off,
2441                };
2442                // `shared` opts into type-id elision: install the program's type registry
2443                // so structs/enums ship a small id instead of their NAMES. OFF by default
2444                // (an empty registry → byte-identical self-describing encode) because a
2445                // relay or a different-program peer would not share the type ids.
2446                let registry = if *shared {
2447                    self.build_wire_type_registry()
2448                } else {
2449                    crate::concurrency::marshal::WireTypeRegistry::new(Vec::new())
2450                };
2451                // `indexed`/`addressable` opts the record list into the random-access struct-view
2452                // LAYOUT (row + field offset tables) so the receiver reaches any (row, field) in
2453                // O(1). It wraps EVERY path below, so it composes with `compressed`, `cached`,
2454                // `shared`, and `unchecked`. OFF by default (the dense columnar form is smaller).
2455                // A PLAIN send (no explicit codec knob) routes through the SHARED negotiated encoder
2456                // both tiers call, so the tree-walker and the VM net path stay byte-identical (the
2457                // cross-tier lock holds). Any explicit knob — `fast`/`packed`/`smallest`/`cached`/
2458                // `shared`/`indexed`/`compressed`/`computed`/`unchecked`/`redundant` — takes its own
2459                // override path below.
2460                let plain = !*computed
2461                    && !*unchecked
2462                    && !*shared
2463                    && !*indexed
2464                    && !*cached
2465                    && !*deduped
2466                    && compression.is_none()
2467                    && matches!(layout, None | Some(SendLayout::Compact));
2468                let bytes = if plain {
2469                    // Pass the program's registry; type-id fires only when the peer's epoch matched
2470                    // (negotiated), so a raw / different-program peer still gets the plain encoding.
2471                    self.netbox.encode_negotiated(&from, &msg, &topic, self.build_wire_type_registry())?
2472                } else {
2473                    // `deduped` wraps the WHOLE encode: a subtree the same value reaches more than once
2474                    // ships once + backrefs, and the receiver rebuilds the sharing. A no-op when the
2475                    // knob is off, so the path below is byte-identical without it.
2476                    with_dedup(*deduped, || with_struct_view(*indexed, || with_type_registry(registry, || -> Result<Vec<u8>, String> {
2477                    // `smallest`/`best`: the message-level auto-tuner measures every dial
2478                    // combination and ships the PROVABLY smallest encoding (never larger than
2479                    // any single knob). It subsumes the numerics/structure/compression knobs,
2480                    // and composes with `shared` (the registry is active here) and `redundant`
2481                    // (FEC shards the result below). The cross-message schema cache (`cached`)
2482                    // is a separate optimization and keeps its own path. `deduped` is excluded here:
2483                    // the auto-tuner runs MANY encode passes, but the dedup id-table is per-encode, so
2484                    // dedup takes the single-pass general path below instead.
2485                    if matches!(layout, Some(SendLayout::Smallest)) && !*cached && !*deduped {
2486                        return with_integrity(integrity, || {
2487                            message_to_wire_best(&from, &msg, WireGoal::Smallest)
2488                        });
2489                    }
2490                    if *cached {
2491                        let cache = self
2492                            .netbox
2493                            .send_schema
2494                            .entry(topic.clone())
2495                            .or_insert_with(WireSchemaCache::content_addressed);
2496                        let mut encode = || message_to_wire_cached(&from, &msg, WireCodec::Native, integrity, cache);
2497                        with_structure(structure, || with_numerics(numerics, || match compression {
2498                            Some(codec) => with_compression_codec(wire_compression_of(*codec), &mut encode),
2499                            None => encode(),
2500                        }))
2501                    } else {
2502                        let mut encode = || message_to_wire_with(&from, &msg, WireCodec::Native, integrity);
2503                        with_structure(structure, || with_numerics(numerics, || match compression {
2504                            Some(codec) => with_compression_codec(wire_compression_of(*codec), &mut encode),
2505                            None => encode(),
2506                        }))
2507                    }
2508                })))?
2509                };
2510                // Seal the encoded message under the active crypto session/suite — identity /
2511                // byte-identical when none is engaged — before FEC/publish (compress → encrypt → FEC).
2512                // A keyed session may fail closed (a one-time pad is exhausted): refuse the send rather
2513                // than transmit plaintext.
2514                let bytes = crate::concurrency::channel::seal_active_checked(bytes)
2515                    .ok_or_else(|| "one-time pad exhausted — message not sent (PNP fail-closed)".to_string())?;
2516                // Advertise our surface to this peer on FIRST contact — on its HANDSHAKE topic, never
2517                // the data topic — so it can negotiate back. Absorbed by the peer's drain.
2518                if let Some(hs) = self.netbox.first_contact_handshake(&topic) {
2519                    self.netbox
2520                        .publish(&crate::concurrency::net_inbox::handshake_topic_for(&topic), hs)?;
2521                }
2522                if matches!(layout, Some(SendLayout::Redundant)) {
2523                    // FEC: split the encoded message into K data + (N−K) parity shards and
2524                    // publish each as its own packet, so a receiver reconstructs the exact
2525                    // message from any K even if some are lost on the link.
2526                    let msg_id = self.netbox.next_msg_id();
2527                    let shards = crate::concurrency::fec::frame_redundant(
2528                        msg_id, &bytes, REDUNDANT_K, REDUNDANT_N,
2529                    )
2530                    .ok_or_else(|| "redundant framing failed".to_string())?;
2531                    for shard in shards {
2532                        self.netbox.publish(&topic, shard)?;
2533                    }
2534                } else {
2535                    self.netbox.publish(&topic, bytes)?;
2536                }
2537                Ok(ControlFlow::Continue)
2538            }
2539
2540            // `Await … from <peer> into x` — block (cooperatively) until a message
2541            // from that peer arrives on our inbox, then bind it to `x`. Messages
2542            // from other peers stay queued for their own `Await`.
2543            Stmt::AwaitMessage { source, into, view, stream } => {
2544                let src = self.evaluate_expr(source).await?;
2545                let want = Self::peer_topic_of(&src)?;
2546                if self.netbox.inbox.is_none() {
2547                    return Err("Await requires a prior Listen to establish an inbox".to_string());
2548                }
2549                // OFFLINE (no relay): the deterministic oracle reads from our own loopback outbox — a
2550                // `Send … to <self>` already delivered the message locally, so `Await` resolves it (no
2551                // real transport needed). A relay-connected node drains the relay instead.
2552                let msg = if *stream {
2553                    self.await_stream_from(&want).await?
2554                } else {
2555                    self.await_message_from(&want, *view).await?
2556                };
2557                self.define(*into, msg);
2558                Ok(ControlFlow::Continue)
2559            }
2560
2561            // `Stream <values> to <peer>` — batch the list into one framed stream message and
2562            // publish it to the peer's inbox; `Await stream from us` deframes it back into a list.
2563            Stmt::StreamMessage { values, destination } => {
2564                let dest = self.evaluate_expr(destination).await?;
2565                let topic = Self::peer_topic_of(&dest)?;
2566                let list = self.evaluate_expr(values).await?;
2567                let items = match &list {
2568                    RuntimeValue::List(rc) => rc.borrow().to_values(),
2569                    other => vec![other.clone()],
2570                };
2571                let from = self.netbox.inbox.as_ref().map(|t| t.to_string()).unwrap_or_default();
2572                let registry = self.build_wire_type_registry();
2573                let blob = crate::concurrency::marshal::with_type_registry(registry, || {
2574                    crate::concurrency::marshal::frame_stream_message(&from, &items)
2575                })?;
2576                // Route through `publish`, which OFFLINE loops the framed stream back into our own inbox
2577                // (a following `Await stream` deframes it) rather than requiring a relay.
2578                self.netbox.publish(&topic, blob)?;
2579                Ok(ControlFlow::Continue)
2580            }
2581
2582            Stmt::MergeCrdt { source, target } => {
2583                let source_val = self.evaluate_expr(source).await?;
2584                let source_fields = match &source_val {
2585                    RuntimeValue::Struct(s) => s.fields.clone(),
2586                    _ => return Err("Merge source must be a struct".to_string()),
2587                };
2588
2589                if let Expr::Identifier(target_sym) = target {
2590                    let mut target_val = self.lookup(*target_sym)?.clone();
2591
2592                    if let RuntimeValue::Struct(ref mut s) = target_val {
2593                        for (field_name, source_field_val) in source_fields {
2594                            let current = s.fields.get(&field_name)
2595                                .cloned()
2596                                .unwrap_or(RuntimeValue::Int(0));
2597
2598                            let merged =
2599                                crate::semantics::arith::crdt_merge_field(&current, source_field_val);
2600                            s.fields.insert(field_name, merged);
2601                        }
2602                        self.assign(*target_sym, target_val)?;
2603                    } else {
2604                        return Err("Merge target must be a struct".to_string());
2605                    }
2606                } else {
2607                    return Err("Merge target must be an identifier".to_string());
2608                }
2609                Ok(ControlFlow::Continue)
2610            }
2611
2612            Stmt::IncreaseCrdt { object, field, amount } => {
2613                let amount_val = self.evaluate_expr(amount).await?;
2614                let amount_int = match amount_val {
2615                    RuntimeValue::Int(n) => n,
2616                    _ => return Err("CRDT increment amount must be an integer".to_string()),
2617                };
2618
2619                if let Expr::Identifier(obj_sym) = object {
2620                    let mut obj_val = self.lookup(*obj_sym)?.clone();
2621
2622                    if let RuntimeValue::Struct(ref mut s) = obj_val {
2623                        let field_name = self.ctx.interner.resolve(*field).to_string();
2624                        let current = s.fields.get(&field_name)
2625                            .cloned()
2626                            .unwrap_or(RuntimeValue::Int(0));
2627
2628                        let new_val =
2629                            crate::semantics::arith::crdt_counter_bump(current, amount_int, &field_name)?;
2630                        s.fields.insert(field_name, new_val);
2631                        self.assign(*obj_sym, obj_val)?;
2632                    } else {
2633                        return Err("Cannot increase field on non-struct value".to_string());
2634                    }
2635                } else {
2636                    return Err("IncreaseCrdt target must be an identifier".to_string());
2637                }
2638                Ok(ControlFlow::Continue)
2639            }
2640
2641            Stmt::DecreaseCrdt { object, field, amount } => {
2642                let amount_val = self.evaluate_expr(amount).await?;
2643                let amount_int = match amount_val {
2644                    RuntimeValue::Int(n) => n,
2645                    _ => return Err("CRDT decrement amount must be an integer".to_string()),
2646                };
2647
2648                if let Expr::Identifier(obj_sym) = object {
2649                    let mut obj_val = self.lookup(*obj_sym)?.clone();
2650
2651                    if let RuntimeValue::Struct(ref mut s) = obj_val {
2652                        let field_name = self.ctx.interner.resolve(*field).to_string();
2653                        let current = s.fields.get(&field_name)
2654                            .cloned()
2655                            .unwrap_or(RuntimeValue::Int(0));
2656
2657                        let new_val = crate::semantics::arith::crdt_counter_bump(
2658                            current,
2659                            amount_int.wrapping_neg(),
2660                            &field_name,
2661                        )?;
2662                        s.fields.insert(field_name, new_val);
2663                        self.assign(*obj_sym, obj_val)?;
2664                    } else {
2665                        return Err("Cannot decrease field on non-struct value".to_string());
2666                    }
2667                } else {
2668                    return Err("DecreaseCrdt target must be an identifier".to_string());
2669                }
2670                Ok(ControlFlow::Continue)
2671            }
2672
2673            Stmt::AppendToSequence { sequence, value } => {
2674                let val = self.evaluate_expr(value).await?;
2675                let seq_val = self.evaluate_expr(sequence).await?;
2676                match &seq_val {
2677                    RuntimeValue::Crdt(rc) => rc.borrow_mut().append(&val)?,
2678                    RuntimeValue::List(_) => {
2679                        crate::semantics::collections::list_push(&seq_val, val)?
2680                    }
2681                    _ => return Err(format!("Cannot append to {}", seq_val.type_name())),
2682                }
2683                Ok(ControlFlow::Continue)
2684            }
2685
2686            Stmt::ResolveConflict { object, field, value } => {
2687                let val = self.evaluate_expr(value).await?;
2688                let obj_val = self.evaluate_expr(object).await?;
2689                let field_name = self.ctx.interner.resolve(*field);
2690                // A real divergent register resolves in place (its `Rc` is shared with the
2691                // struct field); a plain field falls back to a direct assignment.
2692                if let RuntimeValue::Struct(s) = &obj_val {
2693                    if let Some(RuntimeValue::Crdt(rc)) = s.fields.get(field_name) {
2694                        rc.borrow_mut().resolve(&val)?;
2695                        return Ok(ControlFlow::Continue);
2696                    }
2697                }
2698                if let Expr::Identifier(obj_sym) = object {
2699                    let mut owner = self.lookup(*obj_sym)?.clone();
2700                    if let RuntimeValue::Struct(ref mut s) = owner {
2701                        s.fields.insert(field_name.to_string(), val);
2702                        self.assign(*obj_sym, owner)?;
2703                        return Ok(ControlFlow::Continue);
2704                    }
2705                }
2706                Err("Resolve target must be a struct field".to_string())
2707            }
2708
2709            Stmt::Check { subject, predicate, is_capability, object, source_text, .. } => {
2710                // Get the policy registry
2711                let registry = match &self.ctx.policy_registry {
2712                    Some(r) => r,
2713                    None => return Err("Security Check requires policies. Use compiled Rust or add ## Policy block.".to_string()),
2714                };
2715
2716                let subj_val = self.lookup(*subject)?.clone();
2717                let subj_type_name = match &subj_val {
2718                    RuntimeValue::Struct(s) => s.type_name.clone(),
2719                    _ => return Err(format!("Check subject must be a struct, got {}", subj_val.type_name())),
2720                };
2721
2722                // Find the subject type symbol
2723                let subj_type_sym = match self.ctx.interner.lookup(&subj_type_name) {
2724                    Some(sym) => sym,
2725                    None => return Err(format!("Unknown type '{}' in Check statement", subj_type_name)),
2726                };
2727
2728                let passed = if *is_capability {
2729                    // Capability check: "user can publish document"
2730                    let obj_val = match object {
2731                        Some(obj_sym) => Some(self.lookup(*obj_sym)?.clone()),
2732                        None => None,
2733                    };
2734
2735                    let caps = registry.get_capabilities(subj_type_sym);
2736                    let cap = caps
2737                        .and_then(|caps| caps.iter().find(|c| c.action == *predicate));
2738
2739                    match cap {
2740                        Some(cap) => self.evaluate_policy_condition(&cap.condition, &subj_val, obj_val.as_ref()),
2741                        None => {
2742                            let pred_name = self.ctx.interner.resolve(*predicate);
2743                            return Err(format!("No capability '{}' defined for type '{}'", pred_name, subj_type_name));
2744                        }
2745                    }
2746                } else {
2747                    // Predicate check: "user is admin"
2748                    let preds = registry.get_predicates(subj_type_sym);
2749                    let pred_def = preds
2750                        .and_then(|preds| preds.iter().find(|p| p.predicate_name == *predicate));
2751
2752                    match pred_def {
2753                        Some(pred) => self.evaluate_policy_condition(&pred.condition, &subj_val, None),
2754                        None => {
2755                            let pred_name = self.ctx.interner.resolve(*predicate);
2756                            return Err(format!("No predicate '{}' defined for type '{}'", pred_name, subj_type_name));
2757                        }
2758                    }
2759                };
2760
2761                if !passed {
2762                    return Err(format!("Security Check Failed: {}", source_text));
2763                }
2764                Ok(ControlFlow::Continue)
2765            }
2766
2767            // `Connect to "<relay>"` — open the transport: dial the relay and hold
2768            // the connection for `Sync` and peer messaging. Accepts the same
2769            // address surface as the compiled path: a libp2p multiaddr
2770            // (`/ip4/H/tcp/P`) normalizes to the relay's `ws://H:P`; a raw `ws://`
2771            // URL passes through.
2772            Stmt::ConnectTo { address, secure } => {
2773                let raw = self.evaluate_expr(address).await?.to_display_string();
2774                let url = logicaffeine_system::addr::multiaddr_to_ws_url(&raw)
2775                    .map_err(|e| format!("Connect address '{raw}' is not a ws:// URL or supported multiaddr: {e}"))?;
2776                // Activate the one-time-pad session first, so a bad pad fails the connect (fail-closed).
2777                if let Some(bind) = secure {
2778                    self.activate_pnp_session(bind).await?;
2779                }
2780                // OFFLINE mode (the deterministic tree-walker/VM oracles, no relay transport): `Connect`
2781                // is a local no-op — nothing to dial, so `net` stays None and the following ops run
2782                // locally, exactly as `Listen`/`Send`/`Sync` already do offline. A relay-connected driver
2783                // dials for real. The address is still validated so a malformed one errors on both paths.
2784                if !crate::concurrency::net_inbox::net_is_offline() {
2785                    let net = logicaffeine_system::net::Net::connect(&url)
2786                        .await
2787                        .map_err(|e| format!("Connect to relay '{url}' failed: {e}"))?;
2788                    self.netbox.net = Some(net);
2789                }
2790                Ok(ControlFlow::Continue)
2791            }
2792            // `Listen at "<addr>"` — declare this node's identity: subscribe to its
2793            // inbox topic so peers can reach it with `Send … to`. The relay is the
2794            // transport (a browser cannot bind a socket), so this needs a prior
2795            // `Connect`. The address is canonicalized so `/ip4/H/tcp/P` and the
2796            // `ws://H:P` form name the same inbox.
2797            Stmt::Listen { address, secure } => {
2798                let raw = self.evaluate_expr(address).await?.to_display_string();
2799                let topic = logicaffeine_system::addr::canonical_topic(&raw);
2800                let hs_topic = crate::concurrency::net_inbox::handshake_topic_for(&topic);
2801                if let Some(bind) = secure {
2802                    self.activate_pnp_session(bind).await?;
2803                }
2804                // LOCAL/OFFLINE mode (no relay): declare our inbox identity locally and skip the relay
2805                // subscribe — a single node listening on its own address needs no transport. A relay-
2806                // connected node subscribes so peers can reach it. Either way `Listen` never ERRORS.
2807                if let Some(net) = self.netbox.net.as_mut() {
2808                    net.subscribe(&topic).await?;
2809                    // Also receive peers' capability handshakes on our dedicated handshake topic.
2810                    net.subscribe(&hs_topic).await?;
2811                }
2812                self.netbox.inbox = Some(Rc::new(topic));
2813                Ok(ControlFlow::Continue)
2814            }
2815            // `Let r be a PeerAgent at "<addr>"` — a handle to a remote peer; its
2816            // value is the peer's canonical inbox topic. Pure (no I/O); `Send`
2817            // and `Await` do the networking.
2818            Stmt::LetPeerAgent { var, address } => {
2819                let raw = self.evaluate_expr(address).await?.to_display_string();
2820                let topic = logicaffeine_system::addr::canonical_topic(&raw);
2821                self.define(*var, RuntimeValue::Peer(Rc::new(topic)));
2822                Ok(ControlFlow::Continue)
2823            }
2824            Stmt::Sleep { milliseconds } => {
2825                let val = self.evaluate_expr(milliseconds).await?;
2826
2827                // Under the deterministic scheduler (any program with tasks/channels), route
2828                // the sleep through a scheduler timer — the same logical-tick scale as a
2829                // `Select` `After` arm — so it integrates with task scheduling. Blocking on a
2830                // raw host timer here would suspend the task with no scheduler request and
2831                // panic the cooperative driver (and under a non-tokio executor there is no
2832                // reactor at all).
2833                if self.yield_state.is_some() {
2834                    let ticks = match &val {
2835                        RuntimeValue::Int(n) => (*n).max(0) as u64,
2836                        RuntimeValue::Duration(d) => (*d).max(0) as u64,
2837                        _ => return Err(format!("Sleep requires Duration or Int, got {}", val.type_name())),
2838                    };
2839                    if ticks > 0 {
2840                        self.yield_request(BlockingRequest::Sleep(ticks)).await;
2841                    }
2842                    return Ok(ControlFlow::Continue);
2843                }
2844
2845                let nanos = match val {
2846                    RuntimeValue::Duration(nanos) => nanos,
2847                    RuntimeValue::Int(ms) => ms.wrapping_mul(1_000_000), // ms → nanos
2848                    _ => return Err(format!("Sleep requires Duration or Int, got {}", val.type_name())),
2849                };
2850
2851                if nanos > 0 {
2852                    #[cfg(not(target_arch = "wasm32"))]
2853                    {
2854                        // Use tokio re-exported from logicaffeine_system
2855                        logicaffeine_system::tokio::time::sleep(
2856                            std::time::Duration::from_nanos(nanos as u64)
2857                        ).await;
2858                    }
2859                    #[cfg(target_arch = "wasm32")]
2860                    {
2861                        // On WASM, use gloo-timers for async sleep
2862                        let millis = (nanos / 1_000_000) as u32;
2863                        if millis > 0 {
2864                            gloo_timers::future::TimeoutFuture::new(millis).await;
2865                        }
2866                    }
2867                }
2868                Ok(ControlFlow::Continue)
2869            }
2870            // `Sync x on "topic"` — a CRDT sync POINT over the relay: subscribe,
2871            // publish the local counter, then merge whatever has arrived (the same
2872            // `crdt_merge_field` the in-process merge uses). No background task —
2873            // the merge happens here, keeping the tree-walker's linear model.
2874            Stmt::Sync { var, topic } => {
2875                let topic_str = self.evaluate_expr(topic).await?.to_display_string();
2876                let current = self.lookup(*var)?.clone();
2877                // Rich CRDT (ORSet / RGA / MVRegister) → δ-STATE sync: publish only what changed since
2878                // our last sync on this topic (`delta_since`, per field), and merge incoming deltas IN
2879                // PLACE through the shared `Rc`. The first sync ships the full state (delta since
2880                // nothing); every later sync ships a handful of bytes for the NEW entries, never the
2881                // whole collection. Idempotent + commutative, so redelivery / reordering still
2882                // converges. Covers a bare CrdtValue var (field name "") AND every CrdtValue field of a
2883                // `Shared` struct — which is how programs actually hold them.
2884                let crdt_fields: Vec<(String, std::rc::Rc<std::cell::RefCell<crate::semantics::crdt::CrdtValue>>)> =
2885                    match &current {
2886                        RuntimeValue::Crdt(rc) => vec![(String::new(), rc.clone())],
2887                        RuntimeValue::Struct(s) => s
2888                            .fields
2889                            .iter()
2890                            .filter_map(|(k, v)| match v {
2891                                RuntimeValue::Crdt(rc) => Some((k.clone(), rc.clone())),
2892                                _ => None,
2893                            })
2894                            .collect(),
2895                        _ => Vec::new(),
2896                    };
2897                if !crdt_fields.is_empty() {
2898                    let field_key = |name: &str| format!("{topic_str}\u{0}{name}");
2899                    // Each field's delta since the version we last shipped on this topic.
2900                    let mut frame: Vec<(String, Vec<u8>)> = Vec::new();
2901                    for (name, rc) in &crdt_fields {
2902                        let since = self.netbox.sync_versions.get(&field_key(name)).cloned().unwrap_or_default();
2903                        if let Some(d) = rc.borrow().delta_since_bytes(&since) {
2904                            frame.push((name.clone(), d));
2905                        }
2906                    }
2907                    let payload = serde_json::to_vec(&frame).unwrap_or_default();
2908                    // LOCAL/OFFLINE mode (no relay): single-node δ-sync is a no-op — nothing arrives,
2909                    // the local state stands; we still advance the per-field versions below. A relay-
2910                    // connected deployment publishes its delta + merges incoming ones.
2911                    if let Some(net) = self.netbox.net.as_mut() {
2912                        net.subscribe(&topic_str).await?;
2913                        if !frame.is_empty() {
2914                            net.publish(&topic_str, payload)?;
2915                        }
2916                        let incoming = net.drain();
2917                        // Merge every incoming field delta into the matching local CRDT field, in place.
2918                        for (_t, data) in incoming {
2919                            let Ok(fields) = serde_json::from_slice::<Vec<(String, Vec<u8>)>>(&data) else {
2920                                continue;
2921                            };
2922                            for (name, delta) in fields {
2923                                if let Some((_, rc)) = crdt_fields.iter().find(|(n, _)| *n == name) {
2924                                    rc.borrow_mut().apply_delta_bytes(&delta);
2925                                }
2926                            }
2927                        }
2928                    }
2929                    // Record the version we now hold per field, so the next sync ships only later changes.
2930                    for (name, rc) in &crdt_fields {
2931                        let v = rc.borrow().version();
2932                        self.netbox.sync_versions.insert(field_key(name), v);
2933                    }
2934                    return Ok(ControlFlow::Continue);
2935                }
2936                // Encode the counter (Int) or counter-struct (named Int fields) as
2937                // the relay wire form; `None` ⇒ nothing to publish yet.
2938                let publish_bytes = crate::semantics::arith::crdt_to_wire(&current);
2939                // LOCAL/OFFLINE mode: with no relay connected (the playground/test path, and any
2940                // program that never `Connect`ed), a `Sync` is a SINGLE-NODE no-op — the local CRDT
2941                // value stands, deterministically. A real deployment that `Connect`ed to a relay takes
2942                // the transport branch (publish our delta, merge everyone else's). Either way `Sync`
2943                // never ERRORS, so a networked program runs identically on tree-walker, VM, and AOT.
2944                let merged = if let Some(net) = self.netbox.net.as_mut() {
2945                    net.subscribe(&topic_str).await?;
2946                    if let Some(bytes) = publish_bytes {
2947                        net.publish(&topic_str, bytes)?;
2948                    }
2949                    // Merge everything that has arrived since the last sync point, field by field —
2950                    // the same CRDT merge the in-process `Merge` uses.
2951                    let incoming = net.drain();
2952                    let mut merged = current;
2953                    for (_t, data) in incoming {
2954                        merged = crate::semantics::arith::crdt_merge_wire(merged, &data);
2955                    }
2956                    merged
2957                } else {
2958                    current
2959                };
2960                self.assign(*var, merged)?;
2961                Ok(ControlFlow::Continue)
2962            }
2963            // Phase 55: Mount now supported via VFS
2964            Stmt::Mount { var, path } => {
2965                let path_val = self.evaluate_expr(path).await?.to_display_string();
2966                match &self.ctx.vfs {
2967                    Some(vfs) => {
2968                        // Read existing content or create empty
2969                        let content = match vfs.read_to_string(&path_val).await {
2970                            Ok(s) => s,
2971                            Err(_) => String::new(),
2972                        };
2973                        self.define(*var, RuntimeValue::Text(Rc::new(content)));
2974                    }
2975                    None => return Err("VFS not initialized. Use Interpreter::with_vfs()".to_string()),
2976                }
2977                Ok(ControlFlow::Continue)
2978            }
2979
2980            // Phase 54 / T6-T7: Go-like concurrency, driven by the deterministic
2981            // scheduler via the per-task side-channel (`yield_request`).
2982            Stmt::CreatePipe { var, capacity, .. } => {
2983                let cap = capacity.map(|c| c as usize);
2984                let resume = self.yield_request(BlockingRequest::NewChan(cap)).await;
2985                let ch = resume
2986                    .into_chan()
2987                    .ok_or_else(|| "scheduler did not create a channel".to_string())?;
2988                self.define(*var, RuntimeValue::Chan(ch));
2989                Ok(ControlFlow::Continue)
2990            }
2991            Stmt::SendPipe { value, pipe } => {
2992                let ch = self.eval_chan(pipe).await?;
2993                let val = self.evaluate_expr(value).await?;
2994                let payload = marshal::materialize(&val)
2995                    .map_err(|e| format!("cannot send value through a channel: {:?}", e))?;
2996                self.yield_request(BlockingRequest::Send(ch, payload)).await;
2997                Ok(ControlFlow::Continue)
2998            }
2999            Stmt::ReceivePipe { var, pipe } => {
3000                let ch = self.eval_chan(pipe).await?;
3001                let resume = self.yield_request(BlockingRequest::Recv(ch)).await;
3002                let value = marshal::rebuild(resume.into_payload());
3003                self.define(*var, value);
3004                Ok(ControlFlow::Continue)
3005            }
3006            Stmt::LaunchTask { function, args } => {
3007                let mut arg_vals = Vec::with_capacity(args.len());
3008                for a in args.iter() {
3009                    arg_vals.push(self.evaluate_expr(a).await?);
3010                }
3011                let child = self.spawn_child_task(*function, arg_vals);
3012                self.yield_request(BlockingRequest::Spawn(child)).await;
3013                Ok(ControlFlow::Continue)
3014            }
3015            Stmt::LaunchTaskWithHandle { handle, function, args } => {
3016                let mut arg_vals = Vec::with_capacity(args.len());
3017                for a in args.iter() {
3018                    arg_vals.push(self.evaluate_expr(a).await?);
3019                }
3020                let child = self.spawn_child_task(*function, arg_vals);
3021                let resume = self.yield_request(BlockingRequest::Spawn(child)).await;
3022                let tid = resume
3023                    .into_task()
3024                    .ok_or_else(|| "scheduler did not return a task handle".to_string())?;
3025                self.define(*handle, RuntimeValue::TaskHandle(tid));
3026                Ok(ControlFlow::Continue)
3027            }
3028            Stmt::StopTask { handle } => {
3029                let tid = self.eval_task(handle).await?;
3030                self.yield_request(BlockingRequest::Abort(tid)).await;
3031                Ok(ControlFlow::Continue)
3032            }
3033            Stmt::TrySendPipe { value, pipe, result } => {
3034                let ch = self.eval_chan(pipe).await?;
3035                let val = self.evaluate_expr(value).await?;
3036                let payload = marshal::materialize(&val)
3037                    .map_err(|e| format!("cannot send value through a channel: {:?}", e))?;
3038                let resume = self.yield_request(BlockingRequest::TrySend(ch, payload)).await;
3039                let ok = matches!(resume.into_payload(), RtPayload::Bool(true));
3040                if let Some(res) = result {
3041                    self.define(*res, RuntimeValue::Bool(ok));
3042                }
3043                Ok(ControlFlow::Continue)
3044            }
3045            Stmt::TryReceivePipe { var, pipe } => {
3046                let ch = self.eval_chan(pipe).await?;
3047                let resume = self.yield_request(BlockingRequest::TryRecv(ch)).await;
3048                let value = marshal::rebuild(resume.into_payload());
3049                self.define(*var, value);
3050                Ok(ControlFlow::Continue)
3051            }
3052            Stmt::Select { branches } => {
3053                use crate::ast::stmt::SelectBranch;
3054                // Resolve each branch to a runtime arm in declaration order, so the
3055                // scheduler's winning index maps straight back to a branch body.
3056                let mut arms = Vec::with_capacity(branches.len());
3057                for branch in branches.iter() {
3058                    match branch {
3059                        SelectBranch::Receive { pipe, .. } => {
3060                            let ch = self.eval_chan(pipe).await?;
3061                            arms.push(SelectArm::Recv(ch));
3062                        }
3063                        SelectBranch::Timeout { milliseconds, .. } => {
3064                            let ticks = self.eval_select_timeout_ticks(milliseconds).await?;
3065                            arms.push(SelectArm::Timeout(ticks));
3066                        }
3067                    }
3068                }
3069                let resume = self.yield_request(BlockingRequest::Select(arms)).await;
3070                let (arm, payload) = resume
3071                    .into_select()
3072                    .ok_or_else(|| "scheduler did not resolve the select".to_string())?;
3073                match &branches[arm] {
3074                    SelectBranch::Receive { var, body, .. } => {
3075                        self.define(*var, marshal::rebuild(payload));
3076                        self.execute_block(*body).await
3077                    }
3078                    SelectBranch::Timeout { body, .. } => self.execute_block(*body).await,
3079                }
3080            }
3081
3082            // Escape blocks contain raw Rust code and cannot be interpreted
3083            Stmt::Escape { .. } => {
3084                Err(
3085                    "Escape blocks contain raw Rust code and cannot be interpreted. \
3086                     Use `largo build` or `largo run` to compile and run this program."
3087                    .to_string()
3088                )
3089            }
3090
3091            // Dependencies are compilation metadata. No-op in interpreter.
3092            Stmt::Require { .. } => {
3093                Ok(ControlFlow::Continue)
3094            }
3095
3096            // Theorems and definitions are proof-layer declarations verified at
3097            // compile-time, not executed.
3098            Stmt::Theorem(_) | Stmt::Definition(_) | Stmt::Axiom(_) | Stmt::Theory(_) => Ok(ControlFlow::Continue),
3099        }
3100    }
3101
3102    /// Execute a block of statements, returning control flow.
3103    /// Phase 55: Now async.
3104    #[async_recursion(?Send)]
3105    async fn execute_block(&mut self, block: Block<'a>) -> Result<ControlFlow, String> {
3106        self.push_scope();
3107        for stmt in block.iter() {
3108            match self.execute_stmt(stmt).await? {
3109                ControlFlow::Continue => {}
3110                flow => {
3111                    self.pop_scope();
3112                    return Ok(flow);
3113                }
3114            }
3115        }
3116        self.pop_scope();
3117        Ok(ControlFlow::Continue)
3118    }
3119
3120    /// The loud message for a non-exhaustive `Inspect` — no arm matched the
3121    /// scrutinee's actual variant and there was no `Otherwise`. Exhaustiveness
3122    /// or a wildcard is required; a silent no-op hid the missing arm. Kept
3123    /// value-agnostic so it is byte-identical to the VM's compile-time
3124    /// `FailWith` (the VM cannot name the runtime variant at emit time).
3125    fn inspect_unhandled(&self, _target: &RuntimeValue) -> String {
3126        "Inspect has no arm for the value and no Otherwise (matches must be exhaustive)".to_string()
3127    }
3128
3129    /// Execute Inspect (pattern matching).
3130    /// Phase 55: Now async.
3131    /// Phase 102: Extended to handle kernel inductives.
3132    #[async_recursion(?Send)]
3133    async fn execute_inspect(&mut self, target: &RuntimeValue, arms: &[MatchArm<'a>]) -> Result<ControlFlow, String> {
3134        for arm in arms {
3135            // Handle Otherwise (wildcard) case
3136            if arm.variant.is_none() {
3137                let flow = self.execute_block(arm.body).await?;
3138                return Ok(flow);
3139            }
3140
3141            match target {
3142                RuntimeValue::Struct(s) => {
3143                    if let Some(variant) = arm.variant {
3144                        let variant_name = self.ctx.interner.resolve(variant);
3145                        if s.type_name == variant_name {
3146                            self.push_scope();
3147                            for (field_name, binding_name) in &arm.bindings {
3148                                let field_str = self.ctx.interner.resolve(*field_name);
3149                                if let Some(val) = s.fields.get(field_str) {
3150                                    self.define(*binding_name, val.clone());
3151                                }
3152                            }
3153                            let result = self.execute_block(arm.body).await;
3154                            self.pop_scope();
3155                            let flow = result?;
3156                            return Ok(flow);
3157                        }
3158                    }
3159                }
3160
3161                RuntimeValue::Inductive(ind) => {
3162                    if let Some(variant) = arm.variant {
3163                        let variant_name = self.ctx.interner.resolve(variant);
3164                        if ind.constructor == variant_name {
3165                            self.push_scope();
3166                            for (i, (_, binding_name)) in arm.bindings.iter().enumerate() {
3167                                if i < ind.args.len() {
3168                                    self.define(*binding_name, ind.args[i].clone());
3169                                }
3170                            }
3171                            let result = self.execute_block(arm.body).await;
3172                            self.pop_scope();
3173                            let flow = result?;
3174                            return Ok(flow);
3175                        }
3176                    }
3177                }
3178
3179                _ => {}
3180            }
3181        }
3182        Err(self.inspect_unhandled(target))
3183    }
3184
3185    /// Resolve a `Send … to <dest>` / `Await … from <src>` operand to a relay
3186    /// topic: a `PeerAgent` uses its topic; a string is canonicalized as an
3187    /// address; anything else is a type error.
3188    fn peer_topic_of(value: &RuntimeValue) -> Result<String, String> {
3189        match value {
3190            RuntimeValue::Peer(topic) => Ok((**topic).clone()),
3191            RuntimeValue::Text(s) => Ok(logicaffeine_system::addr::canonical_topic(s)),
3192            other => Err(format!(
3193                "Send/Await expects a PeerAgent or address string, got {}",
3194                other.type_name()
3195            )),
3196        }
3197    }
3198
3199    /// Build the wire type registry from the program's struct definitions. A peer running
3200    /// the SAME program derives the identical (content-addressed) ids, so a `Send shared`
3201    /// struct can ship its id instead of its field names and the receiver resolves it.
3202    fn build_wire_type_registry(&self) -> crate::concurrency::marshal::WireTypeRegistry {
3203        let schemas: Vec<(String, Vec<String>)> = self
3204            .ctx
3205            .struct_defs
3206            .iter()
3207            .map(|(name_sym, fields)| {
3208                let type_name = self.ctx.interner.resolve(*name_sym).to_string();
3209                let field_names = fields
3210                    .iter()
3211                    .map(|(fname, _ty, _public)| self.ctx.interner.resolve(*fname).to_string())
3212                    .collect();
3213                (type_name, field_names)
3214            })
3215            .collect();
3216        let enums: Vec<(String, Vec<String>)> = self
3217            .ctx
3218            .enum_defs
3219            .iter()
3220            .map(|(name_sym, ctors)| {
3221                let type_name = self.ctx.interner.resolve(*name_sym).to_string();
3222                let ctor_names = ctors.iter().map(|c| self.ctx.interner.resolve(*c).to_string()).collect();
3223                (type_name, ctor_names)
3224            })
3225            .collect();
3226        crate::concurrency::marshal::WireTypeRegistry::new(schemas).with_enums(enums)
3227    }
3228
3229    /// Block (cooperatively) until a message from `want` arrives on our inbox, returning its payload.
3230    /// Drives the shared [`NetInbox`]: try the buffer, else drain the relay and retry, yielding a
3231    /// macrotask between polls so the browser event loop (and the relay's delivery) keeps running.
3232    /// Messages from other senders stay queued for their own `Await`.
3233    async fn await_message_from(&mut self, want: &str, view: bool) -> Result<RuntimeValue, String> {
3234        loop {
3235            if let Some(v) = self.netbox.try_take_message(want, view) {
3236                return Ok(v);
3237            }
3238            let registry = self.build_wire_type_registry();
3239            self.netbox.drain(registry);
3240            if let Some(v) = self.netbox.try_take_message(want, view) {
3241                return Ok(v);
3242            }
3243            Self::poll_tick().await;
3244        }
3245    }
3246
3247    /// Block until a batch STREAM from `want` arrives, then deframe it into a list (mirrors
3248    /// [`await_message_from`] over a `RecvSlot::Stream`).
3249    async fn await_stream_from(&mut self, want: &str) -> Result<RuntimeValue, String> {
3250        loop {
3251            if let Some(v) = self.netbox.try_take_stream(want) {
3252                return Ok(v);
3253            }
3254            let registry = self.build_wire_type_registry();
3255            self.netbox.drain(registry);
3256            if let Some(v) = self.netbox.try_take_stream(want) {
3257                return Ok(v);
3258            }
3259            Self::poll_tick().await;
3260        }
3261    }
3262
3263    /// One cooperative poll interval, cross-target: a short async sleep that lets
3264    /// the relay deliver and (in the browser) the event loop run between polls.
3265    async fn poll_tick() {
3266        #[cfg(not(target_arch = "wasm32"))]
3267        logicaffeine_system::tokio::time::sleep(std::time::Duration::from_millis(2)).await;
3268        #[cfg(target_arch = "wasm32")]
3269        gloo_timers::future::TimeoutFuture::new(2).await;
3270    }
3271
3272    /// Evaluate an expression to a runtime value.
3273    /// Phase 55: Now async.
3274    #[async_recursion(?Send)]
3275    async fn evaluate_expr(&mut self, expr: &Expr<'a>) -> Result<RuntimeValue, String> {
3276        match expr {
3277            Expr::Literal(lit) => self.evaluate_literal(lit),
3278
3279            Expr::Identifier(sym) => {
3280                let name = self.ctx.interner.resolve(*sym);
3281                // Handle temporal builtins (the NAME wins, even when shadowed)
3282                match name {
3283                    "today" => {
3284                        return Ok(crate::semantics::temporal::today());
3285                    }
3286                    "now" => {
3287                        return Ok(crate::semantics::temporal::now());
3288                    }
3289                    _ => {}
3290                }
3291                self.lookup(*sym).cloned()
3292            }
3293
3294            Expr::BinaryOp { op, left, right } => {
3295                match op {
3296                    BinaryOpKind::And => {
3297                        let left_val = self.evaluate_expr(left).await?;
3298                        if !left_val.is_truthy() {
3299                            return Ok(RuntimeValue::Bool(false));
3300                        }
3301                        let right_val = self.evaluate_expr(right).await?;
3302                        Ok(RuntimeValue::Bool(right_val.is_truthy()))
3303                    }
3304                    BinaryOpKind::Or => {
3305                        let left_val = self.evaluate_expr(left).await?;
3306                        if left_val.is_truthy() {
3307                            return Ok(RuntimeValue::Bool(true));
3308                        }
3309                        let right_val = self.evaluate_expr(right).await?;
3310                        Ok(RuntimeValue::Bool(right_val.is_truthy()))
3311                    }
3312                    _ => {
3313                        let left_val = self.evaluate_expr(left).await?;
3314                        let right_val = self.evaluate_expr(right).await?;
3315                        self.apply_binary_op(*op, left_val, right_val)
3316                    }
3317                }
3318            }
3319
3320            Expr::Call { function, args } => {
3321                self.call_function(*function, args).await
3322            }
3323
3324            Expr::Index { collection, index } => {
3325                let coll_val = self.evaluate_expr(collection).await?;
3326                let idx_val = self.evaluate_expr(index).await?;
3327                crate::semantics::collections::index_get(&coll_val, &idx_val)
3328            }
3329
3330            Expr::Slice { collection, start, end } => {
3331                let coll_val = self.evaluate_expr(collection).await?;
3332                let start_val = self.evaluate_expr(start).await?;
3333                let end_val = self.evaluate_expr(end).await?;
3334                crate::semantics::collections::slice(&coll_val, &start_val, &end_val)
3335            }
3336
3337            Expr::Copy { expr: inner } => {
3338                let val = self.evaluate_expr(inner).await?;
3339                Ok(val.deep_clone())
3340            }
3341
3342            Expr::Give { value } => {
3343                // In interpreter, Give is just semantic - evaluate the value
3344                self.evaluate_expr(value).await
3345            }
3346
3347            Expr::Length { collection } => {
3348                let coll_val = self.evaluate_expr(collection).await?;
3349                crate::semantics::collections::length_of(&coll_val)
3350            }
3351
3352            Expr::Contains { collection, value } => {
3353                let coll_val = self.evaluate_expr(collection).await?;
3354                let val = self.evaluate_expr(value).await?;
3355                crate::semantics::collections::contains(&coll_val, &val)
3356            }
3357
3358            Expr::Union { left, right } => {
3359                let left_val = self.evaluate_expr(left).await?;
3360                let right_val = self.evaluate_expr(right).await?;
3361                crate::semantics::collections::union(&left_val, &right_val)
3362            }
3363
3364            Expr::Intersection { left, right } => {
3365                let left_val = self.evaluate_expr(left).await?;
3366                let right_val = self.evaluate_expr(right).await?;
3367                crate::semantics::collections::intersection(&left_val, &right_val)
3368            }
3369
3370            Expr::List(items) => {
3371                let mut values = Vec::with_capacity(items.len());
3372                for e in items.iter() {
3373                    values.push(self.evaluate_expr(e).await?);
3374                }
3375                Ok(RuntimeValue::List(Rc::new(RefCell::new(ListRepr::from_values(values)))))
3376            }
3377
3378            Expr::Tuple(items) => {
3379                let mut values = Vec::with_capacity(items.len());
3380                for e in items.iter() {
3381                    values.push(self.evaluate_expr(e).await?);
3382                }
3383                Ok(RuntimeValue::Tuple(Rc::new(values)))
3384            }
3385
3386            Expr::Range { start, end } => {
3387                let start_val = self.evaluate_expr(start).await?;
3388                let end_val = self.evaluate_expr(end).await?;
3389                crate::semantics::collections::range(&start_val, &end_val)
3390            }
3391
3392            Expr::FieldAccess { object, field } => {
3393                let obj_val = self.evaluate_expr(object).await?;
3394                match &obj_val {
3395                    RuntimeValue::Struct(s) => {
3396                        let field_name = self.ctx.interner.resolve(*field);
3397                        s.fields.get(field_name).cloned()
3398                            .ok_or_else(|| format!("Field '{}' not found", field_name))
3399                    }
3400                    _ => Err(format!("Cannot access field on {}", obj_val.type_name())),
3401                }
3402            }
3403
3404            Expr::New { type_name, init_fields, .. } => {
3405                let name = self.ctx.interner.resolve(*type_name).to_string();
3406
3407                if name == "Seq" || name == "List" {
3408                    return Ok(RuntimeValue::List(Rc::new(RefCell::new(ListRepr::Ints(Vec::new())))));
3409                }
3410
3411                if name == "Set" || name == "HashSet" {
3412                    return Ok(RuntimeValue::Set(Rc::new(RefCell::new(vec![]))));
3413                }
3414
3415                if name == "Map" || name == "HashMap" {
3416                    return Ok(RuntimeValue::Map(Rc::new(RefCell::new(MapStorage::default()))));
3417                }
3418
3419                let mut fields = HashMap::new();
3420                for (field_sym, field_expr) in init_fields {
3421                    let field_name = self.ctx.interner.resolve(*field_sym).to_string();
3422                    let field_val = self.evaluate_expr(field_expr).await?;
3423                    fields.insert(field_name, field_val);
3424                }
3425
3426                if let Some(def) = self.ctx.struct_defs.get(type_name) {
3427                    for (field_sym, type_sym, _) in def {
3428                        let field_name = self.ctx.interner.resolve(*field_sym).to_string();
3429                        if !fields.contains_key(&field_name) {
3430                            let type_name_str = self.ctx.interner.resolve(*type_sym).to_string();
3431                            let default = match type_name_str.as_str() {
3432                                "Int" => RuntimeValue::Int(0),
3433                                "Float" => RuntimeValue::Float(0.0),
3434                                "Bool" => RuntimeValue::Bool(false),
3435                                "Text" | "String" => RuntimeValue::Text(Rc::new(String::new())),
3436                                "Char" => RuntimeValue::Char('\0'),
3437                                "Byte" => RuntimeValue::Int(0),
3438                                "Seq" | "List" => RuntimeValue::List(Rc::new(RefCell::new(ListRepr::Ints(Vec::new())))),
3439                                "Set" | "HashSet" => RuntimeValue::Set(Rc::new(RefCell::new(vec![]))),
3440                                "Map" | "HashMap" => RuntimeValue::Map(Rc::new(RefCell::new(MapStorage::default()))),
3441                                // A `Shared` struct's CRDT fields default to an empty live
3442                                // CRDT, mirroring the compiled tier's `ORSet`/`RGA` field.
3443                                "SharedSet" | "ORSet" | "SharedSet_AddWins" =>
3444                                    RuntimeValue::Crdt(Rc::new(RefCell::new(
3445                                        crate::semantics::crdt::CrdtValue::new_set(
3446                                            crate::semantics::crdt::next_replica_id())))),
3447                                "SharedSet_RemoveWins" =>
3448                                    RuntimeValue::Crdt(Rc::new(RefCell::new(
3449                                        crate::semantics::crdt::CrdtValue::new_set_remove_wins(
3450                                            crate::semantics::crdt::next_replica_id())))),
3451                                "SharedSequence" | "RGA" | "SharedSequence_YATA" | "CollaborativeSequence" =>
3452                                    RuntimeValue::Crdt(Rc::new(RefCell::new(
3453                                        crate::semantics::crdt::CrdtValue::new_seq(
3454                                            crate::semantics::crdt::next_replica_id())))),
3455                                _ => RuntimeValue::Nothing,
3456                            };
3457                            fields.insert(field_name, default);
3458                        }
3459                    }
3460                }
3461
3462                Ok(RuntimeValue::Struct(Box::new(StructValue { type_name: name, fields })))
3463            }
3464
3465            // Phase 102: Enum variant constructor
3466            // Now creates RuntimeValue::Inductive for unified kernel types
3467            Expr::NewVariant { enum_name, variant, fields } => {
3468                let inductive_type = self.ctx.interner.resolve(*enum_name).to_string();
3469                let constructor = self.ctx.interner.resolve(*variant).to_string();
3470
3471                let mut args = Vec::new();
3472                for (_, field_expr) in fields {
3473                    let field_val = self.evaluate_expr(field_expr).await?;
3474                    args.push(field_val);
3475                }
3476
3477                Ok(RuntimeValue::Inductive(Box::new(InductiveValue {
3478                    inductive_type,
3479                    constructor,
3480                    args,
3481                })))
3482            }
3483
3484            Expr::ManifestOf { .. } => {
3485                Ok(RuntimeValue::List(Rc::new(RefCell::new(ListRepr::Ints(Vec::new())))))
3486            }
3487
3488            Expr::ChunkAt { .. } => {
3489                Ok(RuntimeValue::Nothing)
3490            }
3491
3492            Expr::WithCapacity { value, .. } => {
3493                self.evaluate_expr(value).await
3494            }
3495
3496            Expr::OptionSome { value } => {
3497                self.evaluate_expr(value).await
3498            }
3499
3500            Expr::OptionNone => {
3501                Ok(RuntimeValue::Nothing)
3502            }
3503
3504            Expr::Not { operand } => {
3505                let val = self.evaluate_expr(operand).await?;
3506                crate::semantics::arith::not_value(val)
3507            }
3508
3509            Expr::InterpolatedString(parts) => {
3510                let mut result = String::new();
3511                for part in parts {
3512                    match part {
3513                        crate::ast::stmt::StringPart::Literal(sym) => {
3514                            result.push_str(self.ctx.interner.resolve(*sym));
3515                        }
3516                        crate::ast::stmt::StringPart::Expr { value, format_spec, debug } => {
3517                            let val = self.evaluate_expr(value).await?;
3518                            if *debug {
3519                                let prefix = match value {
3520                                    Expr::Identifier(sym) => self.ctx.interner.resolve(*sym).to_string(),
3521                                    _ => "expr".to_string(),
3522                                };
3523                                result.push_str(&prefix);
3524                                result.push('=');
3525                            }
3526                            if let Some(spec_sym) = format_spec {
3527                                let spec = self.ctx.interner.resolve(*spec_sym);
3528                                result.push_str(&apply_format_spec(&val, spec));
3529                            } else {
3530                                result.push_str(&val.to_display_string());
3531                            }
3532                        }
3533                    }
3534                }
3535                Ok(RuntimeValue::Text(Rc::new(result)))
3536            }
3537
3538            Expr::Escape { .. } => {
3539                Err("Escape expressions contain raw Rust code and cannot be interpreted. \
3540                     Use `largo build` or `largo run` to compile and run this program.".to_string())
3541            }
3542
3543            Expr::Closure { params, body, .. } => {
3544                let free_vars = self.collect_free_vars_in_closure(params, body);
3545                let mut captured_env = HashMap::new();
3546                for sym in &free_vars {
3547                    if let Some(val) = self.task.env.lookup(*sym) {
3548                        captured_env.insert(*sym, val.deep_clone());
3549                    }
3550                }
3551
3552                let body_index = self.ctx.closure_bodies.len();
3553                match body {
3554                    ClosureBody::Expression(expr) => {
3555                        self.ctx.closure_bodies.push(ClosureBodyRef::Expression(expr));
3556                    }
3557                    ClosureBody::Block(block) => {
3558                        self.ctx.closure_bodies.push(ClosureBodyRef::Block(block));
3559                    }
3560                }
3561
3562                let param_names: Vec<Symbol> = params.iter().map(|(name, _)| *name).collect();
3563
3564                Ok(RuntimeValue::Function(Box::new(ClosureValue {
3565                    body_index,
3566                    captured_env,
3567                    param_names,
3568                    generated: None,
3569                })))
3570            }
3571
3572            Expr::CallExpr { callee, args } => {
3573                let callee_val = self.evaluate_expr(callee).await?;
3574                if let RuntimeValue::Function(closure) = callee_val {
3575                    let mut arg_values = Vec::with_capacity(args.len());
3576                    for arg in args.iter() {
3577                        arg_values.push(self.evaluate_expr(arg).await?);
3578                    }
3579                    self.call_closure_value(&closure, arg_values).await
3580                } else {
3581                    Err(format!("Cannot call value of type {}", callee_val.type_name()))
3582                }
3583            }
3584        }
3585    }
3586
3587    /// Evaluate a literal to a runtime value.
3588    fn evaluate_literal(&self, lit: &Literal) -> Result<RuntimeValue, String> {
3589        match lit {
3590            Literal::Number(n) => Ok(RuntimeValue::Int(*n)),
3591            Literal::Float(f) => Ok(RuntimeValue::Float(*f)),
3592            Literal::Text(sym) => Ok(RuntimeValue::Text(Rc::new(self.ctx.interner.resolve(*sym).to_string()))),
3593            Literal::Boolean(b) => Ok(RuntimeValue::Bool(*b)),
3594            Literal::Nothing => Ok(RuntimeValue::Nothing),
3595            Literal::Char(c) => Ok(RuntimeValue::Char(*c)),
3596            Literal::Duration(nanos) => Ok(RuntimeValue::Duration(*nanos)),
3597            Literal::Date(days) => Ok(RuntimeValue::Date(*days)),
3598            Literal::Moment(nanos) => Ok(RuntimeValue::Moment(*nanos)),
3599            Literal::Span { months, days } => Ok(RuntimeValue::Span { months: *months, days: *days }),
3600            Literal::Time(nanos) => Ok(RuntimeValue::Time(*nanos)),
3601        }
3602    }
3603
3604    /// Apply a binary operator (delegates to the shared semantics kernel).
3605    fn apply_binary_op(&self, op: BinaryOpKind, left: RuntimeValue, right: RuntimeValue) -> Result<RuntimeValue, String> {
3606        crate::semantics::arith::binary_op(op, left, right)
3607    }
3608
3609    pub fn values_equal_pub(&self, left: &RuntimeValue, right: &RuntimeValue) -> bool {
3610        self.values_equal(left, right)
3611    }
3612
3613    fn values_equal(&self, left: &RuntimeValue, right: &RuntimeValue) -> bool {
3614        crate::semantics::compare::values_equal(left, right)
3615    }
3616
3617    /// Call a function (built-in or user-defined).
3618    #[async_recursion(?Send)]
3619    async fn call_function(&mut self, function: Symbol, args: &[&'async_recursion Expr<'a>]) -> Result<RuntimeValue, String> {
3620        // Built-in functions — Symbol comparison (integer) instead of string matching
3621        let func_sym = Some(function);
3622        if func_sym == self.ctx.sym_show {
3623            for arg in args {
3624                let val = self.evaluate_expr(arg).await?;
3625                self.emit_output(val.to_display_string());
3626            }
3627            return Ok(RuntimeValue::Nothing);
3628        } else if func_sym == self.ctx.sym_args {
3629            // `args()` system native: the stored argv as a `Seq of Text`,
3630            // mirroring the compiled binary's `env::args()`. Intercepted BEFORE
3631            // the empty native-decl body would be reached, like `show`.
3632            let items: Vec<RuntimeValue> = self
3633                .ctx
3634                .program_args
3635                .iter()
3636                .map(|s| RuntimeValue::Text(Rc::new(s.clone())))
3637                .collect();
3638            return Ok(RuntimeValue::List(Rc::new(RefCell::new(ListRepr::from_values(items)))));
3639        } else if let Some(id) = self.builtin_id(function) {
3640            // Arity is checked BEFORE evaluating arguments (kernel rule).
3641            crate::semantics::builtins::check_arity(id, args.len())?;
3642            // `format` reads only its first argument; preserve its laziness.
3643            let vals = if id == crate::semantics::builtins::BuiltinId::Format {
3644                match args.first() {
3645                    Some(a) => vec![self.evaluate_expr(a).await?],
3646                    None => Vec::new(),
3647                }
3648            } else {
3649                let mut v = Vec::with_capacity(args.len());
3650                for arg in args {
3651                    v.push(self.evaluate_expr(arg).await?);
3652                }
3653                v
3654            };
3655            return crate::semantics::builtins::call_builtin(id, vals);
3656        }
3657
3658        // User-defined function lookup — extract metadata without cloning params
3659        if let Some(func) = self.ctx.functions.get(&function) {
3660            let param_count = func.params.len();
3661            let body = func.body;
3662
3663            if args.len() != param_count {
3664                return Err(format!(
3665                    "Function {} expects {} arguments, got {}",
3666                    self.ctx.interner.resolve(function),
3667                    param_count,
3668                    args.len()
3669                ));
3670            }
3671
3672            // Evaluate arguments before pushing scope
3673            let mut arg_values = Vec::with_capacity(param_count);
3674            for arg in args {
3675                arg_values.push(self.evaluate_expr(arg).await?);
3676            }
3677
3678            // Bind parameters in a FRESH frame — the lexical barrier: the body
3679            // sees params, its own bindings, and globals; never caller locals.
3680            if self.task.call_depth >= crate::semantics::MAX_CALL_DEPTH {
3681            return Err(crate::semantics::CALL_DEPTH_ERR.to_string());
3682        }
3683        self.task.call_depth += 1;
3684        self.task.env.push_frame();
3685            for i in 0..param_count {
3686                let param_name = self.ctx.functions[&function].params[i].0;
3687                self.task.env.define(param_name, std::mem::replace(&mut arg_values[i], RuntimeValue::Nothing));
3688            }
3689
3690            // Execute function body
3691            // TCO on the async path — mirror of the sync twin in
3692            // `call_function_sync` (constant-stack self-tail-calls).
3693            let prev_tco = self.task.tco_fn_async.replace(function);
3694            let prev_repeat = std::mem::replace(&mut self.task.repeat_depth_async, 0);
3695            let mut return_value = RuntimeValue::Nothing;
3696            let mut body_err = None;
3697            'tco: loop {
3698                self.task.pending_tail_call_async = None;
3699                let mut idx = 0;
3700                while idx < body.len() {
3701                    if idx + 1 < body.len() {
3702                        if let Some(call_args) = crate::tail_call::tail_pair_args(
3703                            &body[idx],
3704                            &body[idx + 1],
3705                            function,
3706                            param_count,
3707                        ) {
3708                            let mut vals = Vec::with_capacity(call_args.len());
3709                            let mut perr = None;
3710                            for a in call_args {
3711                                match self.evaluate_expr(a).await {
3712                                    Ok(v) => vals.push(v),
3713                                    Err(e) => {
3714                                        perr = Some(e);
3715                                        break;
3716                                    }
3717                                }
3718                            }
3719                            match perr {
3720                                Some(e) => body_err = Some(e),
3721                                None => self.task.pending_tail_call_async = Some(vals),
3722                            }
3723                            break;
3724                        }
3725                    }
3726                    match self.execute_stmt(&body[idx]).await {
3727                        Ok(ControlFlow::Return(val)) => {
3728                            return_value = val;
3729                            break;
3730                        }
3731                        Ok(ControlFlow::Break) => break,
3732                        Ok(ControlFlow::Continue) => {}
3733                        Err(e) => {
3734                            body_err = Some(e);
3735                            break;
3736                        }
3737                    }
3738                    idx += 1;
3739                }
3740                if body_err.is_some() {
3741                    break 'tco;
3742                }
3743                match self.task.pending_tail_call_async.take() {
3744                    Some(new_args) => {
3745                        self.task.env.pop_frame();
3746                        self.task.env.push_frame();
3747                        for (i, v) in new_args.into_iter().enumerate() {
3748                            let param_name = self.ctx.functions[&function].params[i].0;
3749                            self.task.env.define(param_name, v);
3750                        }
3751                        continue 'tco;
3752                    }
3753                    None => break 'tco,
3754                }
3755            }
3756            self.task.repeat_depth_async = prev_repeat;
3757            self.task.tco_fn_async = prev_tco;
3758
3759            self.task.env.pop_frame();
3760        self.task.call_depth -= 1;
3761            match body_err {
3762                Some(e) => Err(e),
3763                None => Ok(return_value),
3764            }
3765        } else {
3766            // Fallback: check if the function name is a variable holding a closure
3767            let maybe_closure = self.task.env.lookup(function)
3768                .and_then(|v| if let RuntimeValue::Function(c) = v { Some((**c).clone()) } else { None });
3769
3770            if let Some(closure) = maybe_closure {
3771                let mut arg_values = Vec::with_capacity(args.len());
3772                for arg in args {
3773                    arg_values.push(self.evaluate_expr(arg).await?);
3774                }
3775                self.call_closure_value(&closure, arg_values).await
3776            } else {
3777                Err(format!("Unknown function: {}", self.ctx.interner.resolve(function)))
3778            }
3779        }
3780    }
3781
3782    /// Call a function with pre-evaluated RuntimeValue arguments.
3783    /// Used by Give and Show statements where the object is already evaluated.
3784    /// Build the suspend-future for a concurrency request. The returned future
3785    /// owns its `Rc` side-channel, so awaiting it does not borrow `self`.
3786    fn yield_request(&self, req: BlockingRequest<'a>) -> YieldFuture<'a> {
3787        let ys = self
3788            .yield_state
3789            .clone()
3790            .expect("concurrency op executed outside a scheduler context");
3791        ys.borrow_mut().request = Some(req);
3792        YieldFuture::new(ys)
3793    }
3794
3795    /// Evaluate an expression to a channel handle.
3796    async fn eval_chan(&mut self, expr: &Expr<'a>) -> Result<ChanId, String> {
3797        match self.evaluate_expr(expr).await? {
3798            RuntimeValue::Chan(id) => Ok(id),
3799            other => Err(format!("expected a channel, found {}", other.type_name())),
3800        }
3801    }
3802
3803    /// Evaluate an expression to a task handle.
3804    async fn eval_task(&mut self, expr: &Expr<'a>) -> Result<TaskId, String> {
3805        match self.evaluate_expr(expr).await? {
3806            RuntimeValue::TaskHandle(id) => Ok(id),
3807            other => Err(format!("expected a task handle, found {}", other.type_name())),
3808        }
3809    }
3810
3811    /// Evaluate a `Select` timeout expression to a non-negative logical tick
3812    /// count for the scheduler's timer wheel. A bare integer is read as whole
3813    /// seconds (matching the compiled `Duration::from_secs`), a duration as its
3814    /// own magnitude, and a calendar span as whole seconds.
3815    async fn eval_select_timeout_ticks(&mut self, expr: &Expr<'a>) -> Result<u64, String> {
3816        let ticks = match self.evaluate_expr(expr).await? {
3817            RuntimeValue::Int(n) => n.max(0) as u64,
3818            RuntimeValue::Duration(d) => d.max(0) as u64,
3819            RuntimeValue::Span { months, days } => {
3820                (((months as i64) * 30 + days as i64) * 86_400).max(0) as u64
3821            }
3822            other => {
3823                return Err(format!(
3824                    "select timeout must be a number or duration, found {}",
3825                    other.type_name()
3826                ))
3827            }
3828        };
3829        Ok(ticks)
3830    }
3831
3832    /// Build a child interpreter task that runs `function(args)`, sharing this
3833    /// interpreter's (cloned) context with a fresh per-task state + side-channel.
3834    fn spawn_child_task(
3835        &self,
3836        function: Symbol,
3837        args: Vec<RuntimeValue>,
3838    ) -> Box<dyn logicaffeine_runtime::Task<'a> + 'a> {
3839        let ys: Yield<'a> = Rc::new(RefCell::new(YieldState::new()));
3840        let mut child = Interpreter {
3841            ctx: self.ctx.clone(),
3842            task: TaskState::new(),
3843            output: Vec::new(),
3844            yield_state: Some(ys.clone()),
3845            netbox: crate::concurrency::net_inbox::NetInbox::new(),
3846        };
3847        let fut = Box::pin(async move {
3848            child.call_function_with_values(function, args).await.map(|_| ())
3849        });
3850        Box::new(InterpreterTask::new(fut, ys, None))
3851    }
3852
3853    #[async_recursion(?Send)]
3854    async fn call_function_with_values(&mut self, function: Symbol, mut args: Vec<RuntimeValue>) -> Result<RuntimeValue, String> {
3855        // Handle built-in "show" via Symbol comparison
3856        if Some(function) == self.ctx.sym_show {
3857            for val in args {
3858                self.emit_output(val.to_display_string());
3859            }
3860            return Ok(RuntimeValue::Nothing);
3861        }
3862
3863        if let Some(func) = self.ctx.functions.get(&function) {
3864            let param_count = func.params.len();
3865            let body = func.body;
3866
3867            if args.len() != param_count {
3868                return Err(format!(
3869                    "Function {} expects {} arguments, got {}",
3870                    self.ctx.interner.resolve(function), param_count, args.len()
3871                ));
3872            }
3873
3874            if self.task.call_depth >= crate::semantics::MAX_CALL_DEPTH {
3875            return Err(crate::semantics::CALL_DEPTH_ERR.to_string());
3876        }
3877        self.task.call_depth += 1;
3878        self.task.env.push_frame();
3879            for i in 0..param_count {
3880                let param_name = self.ctx.functions[&function].params[i].0;
3881                self.task.env.define(param_name, std::mem::replace(&mut args[i], RuntimeValue::Nothing));
3882            }
3883
3884            // TCO on the async path — mirror of the sync twin in
3885            // `call_function_sync` (constant-stack self-tail-calls).
3886            let prev_tco = self.task.tco_fn_async.replace(function);
3887            let prev_repeat = std::mem::replace(&mut self.task.repeat_depth_async, 0);
3888            let mut return_value = RuntimeValue::Nothing;
3889            let mut body_err = None;
3890            'tco: loop {
3891                self.task.pending_tail_call_async = None;
3892                let mut idx = 0;
3893                while idx < body.len() {
3894                    if idx + 1 < body.len() {
3895                        if let Some(call_args) = crate::tail_call::tail_pair_args(
3896                            &body[idx],
3897                            &body[idx + 1],
3898                            function,
3899                            param_count,
3900                        ) {
3901                            let mut vals = Vec::with_capacity(call_args.len());
3902                            let mut perr = None;
3903                            for a in call_args {
3904                                match self.evaluate_expr(a).await {
3905                                    Ok(v) => vals.push(v),
3906                                    Err(e) => {
3907                                        perr = Some(e);
3908                                        break;
3909                                    }
3910                                }
3911                            }
3912                            match perr {
3913                                Some(e) => body_err = Some(e),
3914                                None => self.task.pending_tail_call_async = Some(vals),
3915                            }
3916                            break;
3917                        }
3918                    }
3919                    match self.execute_stmt(&body[idx]).await {
3920                        Ok(ControlFlow::Return(val)) => {
3921                            return_value = val;
3922                            break;
3923                        }
3924                        Ok(ControlFlow::Break) => break,
3925                        Ok(ControlFlow::Continue) => {}
3926                        Err(e) => {
3927                            body_err = Some(e);
3928                            break;
3929                        }
3930                    }
3931                    idx += 1;
3932                }
3933                if body_err.is_some() {
3934                    break 'tco;
3935                }
3936                match self.task.pending_tail_call_async.take() {
3937                    Some(new_args) => {
3938                        self.task.env.pop_frame();
3939                        self.task.env.push_frame();
3940                        for (i, v) in new_args.into_iter().enumerate() {
3941                            let param_name = self.ctx.functions[&function].params[i].0;
3942                            self.task.env.define(param_name, v);
3943                        }
3944                        continue 'tco;
3945                    }
3946                    None => break 'tco,
3947                }
3948            }
3949            self.task.repeat_depth_async = prev_repeat;
3950            self.task.tco_fn_async = prev_tco;
3951
3952            self.task.env.pop_frame();
3953        self.task.call_depth -= 1;
3954            match body_err {
3955                Some(e) => Err(e),
3956                None => Ok(return_value),
3957            }
3958        } else {
3959            let maybe_closure = self.task.env.lookup(function)
3960                .and_then(|v| if let RuntimeValue::Function(c) = v { Some((**c).clone()) } else { None });
3961
3962            if let Some(closure) = maybe_closure {
3963                self.call_closure_value(&closure, args).await
3964            } else {
3965                Err(format!("Unknown function: {}", self.ctx.interner.resolve(function)))
3966            }
3967        }
3968    }
3969
3970    /// Map a function symbol to its kernel builtin, via the cached symbols.
3971    fn builtin_id(&self, f: Symbol) -> Option<crate::semantics::builtins::BuiltinId> {
3972        use crate::semantics::builtins::BuiltinId as B;
3973        let s = Some(f);
3974        if s == self.ctx.sym_length {
3975            Some(B::Length)
3976        } else if s == self.ctx.sym_format {
3977            Some(B::Format)
3978        } else if s == self.ctx.sym_parse_int {
3979            Some(B::ParseInt)
3980        } else if s == self.ctx.sym_parse_float {
3981            Some(B::ParseFloat)
3982        } else if s == self.ctx.sym_chr {
3983            Some(B::Chr)
3984        } else if s == self.ctx.sym_abs {
3985            Some(B::Abs)
3986        } else if s == self.ctx.sym_sqrt {
3987            Some(B::Sqrt)
3988        } else if s == self.ctx.sym_min {
3989            Some(B::Min)
3990        } else if s == self.ctx.sym_max {
3991            Some(B::Max)
3992        } else if s == self.ctx.sym_floor {
3993            Some(B::Floor)
3994        } else if s == self.ctx.sym_ceil {
3995            Some(B::Ceil)
3996        } else if s == self.ctx.sym_round {
3997            Some(B::Round)
3998        } else if s == self.ctx.sym_pow {
3999            Some(B::Pow)
4000        } else if s == self.ctx.sym_copy {
4001            Some(B::Copy)
4002        } else if s == self.ctx.sym_count_ones {
4003            Some(B::CountOnes)
4004        } else {
4005            // Fall back to name-based resolution for any builtin NOT pre-interned as a ctx
4006            // symbol (e.g. `run_accepted`). The pre-interned checks above are a fast path;
4007            // this keeps the tree-walker resolving exactly the builtin set the VM does via
4008            // `builtin_from_name` — cross-tier consistent, no silent "Unknown function".
4009            crate::semantics::builtins::builtin_from_name(self.ctx.interner.resolve(f))
4010        }
4011    }
4012
4013    // Scope management
4014
4015    fn push_scope(&mut self) {
4016        self.task.env.push_scope();
4017    }
4018
4019    fn pop_scope(&mut self) {
4020        self.task.env.pop_scope();
4021    }
4022
4023    fn define(&mut self, name: Symbol, value: RuntimeValue) {
4024        self.task.env.define(name, value);
4025    }
4026
4027    fn assign(&mut self, name: Symbol, value: RuntimeValue) -> Result<(), String> {
4028        if self.task.env.assign(name, value) {
4029            Ok(())
4030        } else {
4031            Err(format!("Undefined variable: {}", self.ctx.interner.resolve(name)))
4032        }
4033    }
4034
4035    fn lookup(&self, name: Symbol) -> Result<&RuntimeValue, String> {
4036        self.task.env.lookup(name)
4037            .ok_or_else(|| format!("Undefined variable: {}", self.ctx.interner.resolve(name)))
4038    }
4039
4040    /// True if `sym` is a `mutable` parameter of the function whose body is
4041    /// currently executing. Such a parameter passes by reference (Mutable Value
4042    /// Semantics escape hatch), so its mutations must reach the caller's
4043    /// collection in place — copy-on-write is suppressed for it.
4044    fn is_mutable_param(&self, sym: Symbol) -> bool {
4045        let Some(fn_sym) = self.task.tco_fn_sync.or(self.task.tco_fn_async) else {
4046            return false;
4047        };
4048        let Some(fdef) = self.ctx.functions.get(&fn_sym) else {
4049            return false;
4050        };
4051        fdef.params
4052            .iter()
4053            .any(|(p, ty)| *p == sym && matches!(ty, crate::ast::stmt::TypeExpr::Mutable { .. }))
4054    }
4055
4056    /// Copy-on-write for value semantics. Before mutating the collection bound to
4057    /// `sym`, ensure it is uniquely owned: if another binding shares the same
4058    /// allocation (`Rc` strong count > 1) — from `Let b be a`, a plain parameter,
4059    /// or storage inside another collection — replace `sym` with a deep copy so
4060    /// the mutation cannot be observed through that other binding. Sound: it
4061    /// never UNDER-copies (any alias bumps the count), so it can only ever copy
4062    /// when it might otherwise alias. A `mutable` parameter is exempt — it
4063    /// deliberately mutates the caller's collection in place.
4064    fn ensure_collection_owned(&mut self, sym: Symbol) {
4065        // MVS migration gate. Copy-on-write value semantics for collections must
4066        // land in LOCKSTEP across the tree-walker, VM, and AOT: the debug shadow
4067        // oracle (ui_bridge) cross-checks the VM against the tree-walker on every
4068        // program, so flipping one engine alone makes every aliasing-observable
4069        // program diverge. This tree-walker COW is implemented and validated;
4070        // it stays gated (off by default) until the VM + AOT flip together.
4071        if !crate::semantics::collections::value_semantics_enabled() {
4072            return;
4073        }
4074        if self.is_mutable_param(sym) {
4075            return;
4076        }
4077        let shared = match self.task.env.lookup(sym) {
4078            Some(RuntimeValue::List(rc)) => Rc::strong_count(rc) > 1,
4079            Some(RuntimeValue::Map(rc)) => Rc::strong_count(rc) > 1,
4080            Some(RuntimeValue::Set(rc)) => Rc::strong_count(rc) > 1,
4081            _ => false,
4082        };
4083        if shared {
4084            let owned = self.task.env.lookup(sym).map(|v| v.deep_clone());
4085            if let Some(owned) = owned {
4086                self.task.env.assign(sym, owned);
4087            }
4088        }
4089    }
4090
4091    /// Evaluate a policy condition against a subject value.
4092    fn evaluate_policy_condition(
4093        &self,
4094        condition: &PolicyCondition,
4095        subject: &RuntimeValue,
4096        object: Option<&RuntimeValue>,
4097    ) -> bool {
4098        crate::semantics::policy::evaluate_policy_condition(
4099            self.ctx.policy_registry.as_ref(),
4100            self.ctx.interner,
4101            condition,
4102            subject,
4103            object,
4104        )
4105    }
4106
4107    /// The program's global bindings after execution, as sorted
4108    /// `(name, type, value)` rows — the substrate for the REPL's `:vars`
4109    /// inspection (see [`crate::repl::ReplSession::vars`]).
4110    pub fn global_bindings(&self) -> Vec<(String, String, String)> {
4111        let mut rows: Vec<(String, String, String)> = self
4112            .task
4113            .env
4114            .globals
4115            .iter()
4116            .map(|(sym, value)| {
4117                (
4118                    self.ctx.interner.resolve(*sym).to_string(),
4119                    value.type_name().to_string(),
4120                    value.to_display_string(),
4121                )
4122            })
4123            .collect();
4124        rows.sort();
4125        rows
4126    }
4127
4128    // =========================================================================
4129    // Sync execution path — eliminates async/Future overhead for pure programs
4130    // =========================================================================
4131
4132    /// Execute a program synchronously (no async/Future allocation).
4133    /// Use when `needs_async(stmts)` returns false.
4134    pub fn run_sync(&mut self, stmts: &[Stmt<'a>]) -> Result<(), String> {
4135        // Hermetic program start: no ambient exchange rates carried in (mirrors a fresh AOT process).
4136        logicaffeine_base::money::clear_ambient_rates();
4137        for stmt in stmts {
4138            match self.execute_stmt_sync(stmt)? {
4139                ControlFlow::Return(_) => break,
4140                ControlFlow::Break => break,
4141                ControlFlow::Continue => {}
4142            }
4143        }
4144        Ok(())
4145    }
4146
4147    fn execute_stmt_sync(&mut self, stmt: &Stmt<'a>) -> Result<ControlFlow, String> {
4148        match stmt {
4149            Stmt::Let { var, value, .. } => {
4150                let val = self.evaluate_expr_sync(value)?;
4151                self.define(*var, val);
4152                Ok(ControlFlow::Continue)
4153            }
4154
4155            Stmt::Set { target, value } => {
4156                let val = self.evaluate_expr_sync(value)?;
4157                self.assign(*target, val)?;
4158                Ok(ControlFlow::Continue)
4159            }
4160
4161            Stmt::Call { function, args } => {
4162                self.call_function_sync(*function, args)?;
4163                Ok(ControlFlow::Continue)
4164            }
4165
4166            Stmt::If { cond, then_block, else_block } => {
4167                let condition = self.evaluate_expr_sync(cond)?;
4168                if condition.is_truthy() {
4169                    let flow = self.execute_block_sync(then_block)?;
4170                    if !matches!(flow, ControlFlow::Continue) {
4171                        return Ok(flow);
4172                    }
4173                } else if let Some(else_stmts) = else_block {
4174                    let flow = self.execute_block_sync(else_stmts)?;
4175                    if !matches!(flow, ControlFlow::Continue) {
4176                        return Ok(flow);
4177                    }
4178                }
4179                Ok(ControlFlow::Continue)
4180            }
4181
4182            Stmt::While { cond, body, .. } => {
4183                loop {
4184                    let condition = self.evaluate_expr_sync(cond)?;
4185                    if !condition.is_truthy() {
4186                        break;
4187                    }
4188                    match self.execute_block_sync(body)? {
4189                        ControlFlow::Break => break,
4190                        ControlFlow::Return(v) => return Ok(ControlFlow::Return(v)),
4191                        ControlFlow::Continue => {}
4192                    }
4193                }
4194                Ok(ControlFlow::Continue)
4195            }
4196
4197            Stmt::Repeat { pattern, iterable, body } => {
4198                use crate::ast::stmt::Pattern;
4199
4200                let iter_val = self.evaluate_expr_sync(iterable)?;
4201                let items = crate::semantics::collections::iteration_snapshot(&iter_val)?;
4202
4203                self.push_scope();
4204                // A `Repeat` owns a live iterator: suppress TCO of any self-call
4205                // detected inside it (jumping to the body start would abandon the
4206                // iterator), matching the VM's `is_repeat` guard exactly.
4207                self.task.repeat_depth_sync += 1;
4208                for item in items {
4209                    match pattern {
4210                        Pattern::Identifier(sym) => {
4211                            self.define(*sym, item);
4212                        }
4213                        Pattern::Tuple(syms) => {
4214                            if let RuntimeValue::Tuple(ref tuple_vals) = item {
4215                                if syms.len() != tuple_vals.len() {
4216                                    self.task.repeat_depth_sync -= 1;
4217                                    return Err(format!(
4218                                        "Cannot bind a {}-tuple to {} names",
4219                                        tuple_vals.len(),
4220                                        syms.len()
4221                                    ));
4222                                }
4223                                for (sym, val) in syms.iter().zip(tuple_vals.iter()) {
4224                                    self.define(*sym, val.clone());
4225                                }
4226                            } else {
4227                                self.task.repeat_depth_sync -= 1;
4228                                return Err(format!("Expected tuple for pattern, got {}", item.type_name()));
4229                            }
4230                        }
4231                    }
4232
4233                    match self.execute_block_sync(body)? {
4234                        ControlFlow::Break => break,
4235                        ControlFlow::Return(v) => {
4236                            self.task.repeat_depth_sync -= 1;
4237                            self.pop_scope();
4238                            return Ok(ControlFlow::Return(v));
4239                        }
4240                        ControlFlow::Continue => {}
4241                    }
4242                }
4243                self.task.repeat_depth_sync -= 1;
4244                self.pop_scope();
4245                Ok(ControlFlow::Continue)
4246            }
4247
4248            Stmt::Return { value } => {
4249                // A direct self-tail-call `Return self(args)` becomes a loop-back
4250                // in `call_function_sync`: signal it via `pending_tail_call` and
4251                // return control to the body driver (which sees the sentinel and
4252                // restarts instead of using the value). Args are evaluated here —
4253                // a nested self-call in an argument stays ordinary recursion.
4254                if let Some(expr) = value {
4255                    if let Some(call_args) = self.self_tail_call_args_sync(*expr) {
4256                        let mut vals = Vec::with_capacity(call_args.len());
4257                        for a in call_args {
4258                            vals.push(self.evaluate_expr_sync(a)?);
4259                        }
4260                        self.task.pending_tail_call = Some(vals);
4261                        return Ok(ControlFlow::Return(RuntimeValue::Nothing));
4262                    }
4263                }
4264                let ret_val = match value {
4265                    Some(expr) => self.evaluate_expr_sync(expr)?,
4266                    None => RuntimeValue::Nothing,
4267                };
4268                Ok(ControlFlow::Return(ret_val))
4269            }
4270
4271            Stmt::Break => Ok(ControlFlow::Break),
4272
4273            Stmt::FunctionDef { name, params, body, return_type, .. } => {
4274                let func = FunctionDef {
4275                    params: params.clone(),
4276                    body: *body,
4277                    return_type: *return_type,
4278                };
4279                self.ctx.functions.insert(*name, func);
4280                Ok(ControlFlow::Continue)
4281            }
4282
4283            Stmt::StructDef { name, fields, .. } => {
4284                self.ctx.struct_defs.insert(*name, fields.clone());
4285                Ok(ControlFlow::Continue)
4286            }
4287
4288            Stmt::SetField { object, field, value } => {
4289                let new_val = self.evaluate_expr_sync(value)?;
4290                if let Expr::Identifier(obj_sym) = object {
4291                    let mut obj_val = self.lookup(*obj_sym)?.clone();
4292                    if let RuntimeValue::Struct(ref mut s) = obj_val {
4293                        let field_name = self.ctx.interner.resolve(*field).to_string();
4294                        s.fields.insert(field_name, new_val);
4295                        self.assign(*obj_sym, obj_val)?;
4296                    } else {
4297                        return Err(format!("Cannot set field on non-struct value"));
4298                    }
4299                } else {
4300                    return Err("SetField target must be an identifier".to_string());
4301                }
4302                Ok(ControlFlow::Continue)
4303            }
4304
4305            Stmt::Push { value, collection } => {
4306                let val = self.evaluate_expr_sync(value)?;
4307                if let Expr::Identifier(coll_sym) = collection {
4308                    self.ensure_collection_owned(*coll_sym);
4309                    let coll_val = self.lookup(*coll_sym)?;
4310                    crate::semantics::collections::list_push(&coll_val, val)?;
4311                } else if let Expr::FieldAccess { object, field } = collection {
4312                    if let Expr::Identifier(obj_sym) = *object {
4313                        let obj_val = self.lookup(*obj_sym)?;
4314                        let field_name = self.ctx.interner.resolve(*field);
4315                        crate::semantics::collections::push_to_struct_field(&obj_val, field_name, val)?;
4316                    } else {
4317                        return Err("Push to nested field access not supported".to_string());
4318                    }
4319                } else {
4320                    // Any place expression is an l-value; see the async Push
4321                    // handler for the aliasing rationale.
4322                    let coll_val = self.evaluate_expr_sync(collection)?;
4323                    crate::semantics::collections::list_push(&coll_val, val)?;
4324                }
4325                Ok(ControlFlow::Continue)
4326            }
4327
4328            Stmt::Pop { collection, into } => {
4329                if let Expr::Identifier(coll_sym) = collection {
4330                    self.ensure_collection_owned(*coll_sym);
4331                    let coll_val = self.lookup(*coll_sym)?;
4332                    let popped = crate::semantics::collections::list_pop(&coll_val)?;
4333                    if let Some(into_var) = into {
4334                        self.define(*into_var, popped);
4335                    }
4336                } else {
4337                    return Err("Pop collection must be an identifier".to_string());
4338                }
4339                Ok(ControlFlow::Continue)
4340            }
4341
4342            Stmt::Add { value, collection } => {
4343                let val = self.evaluate_expr_sync(value)?;
4344                if let Expr::Identifier(coll_sym) = collection {
4345                    self.ensure_collection_owned(*coll_sym);
4346                }
4347                // Resolve the collection generally — a bare variable or a (CRDT/Set)
4348                // struct field, e.g. `Add "Alice" to p's guests`.
4349                let coll_val = self.evaluate_expr_sync(collection)?;
4350                crate::semantics::collections::set_add(&coll_val, val)?;
4351                Ok(ControlFlow::Continue)
4352            }
4353
4354            Stmt::Remove { value, collection } => {
4355                let val = self.evaluate_expr_sync(value)?;
4356                if let Expr::Identifier(coll_sym) = collection {
4357                    self.ensure_collection_owned(*coll_sym);
4358                }
4359                let coll_val = self.evaluate_expr_sync(collection)?;
4360                crate::semantics::collections::remove_from(&coll_val, &val)?;
4361                Ok(ControlFlow::Continue)
4362            }
4363
4364            Stmt::SetIndex { collection, index, value } => {
4365                let idx_val = self.evaluate_expr_sync(index)?;
4366                let new_val = self.evaluate_expr_sync(value)?;
4367                if let Expr::Identifier(coll_sym) = collection {
4368                    // Struct field set via index syntax (mirrors the read side); see the
4369                    // async SetIndex handler for rationale.
4370                    if let RuntimeValue::Text(field) = &idx_val {
4371                        let cur = self.lookup(*coll_sym)?.clone();
4372                        if let RuntimeValue::Struct(mut s) = cur {
4373                            s.fields.insert(field.to_string(), new_val);
4374                            self.assign(*coll_sym, RuntimeValue::Struct(s))?;
4375                            return Ok(ControlFlow::Continue);
4376                        }
4377                    }
4378                    self.ensure_collection_owned(*coll_sym);
4379                    let coll_val = self.lookup(*coll_sym)?;
4380                    crate::semantics::collections::index_set(&coll_val, &idx_val, new_val)?;
4381                } else {
4382                    // Any place expression is an l-value; see the async
4383                    // SetIndex handler for rationale.
4384                    let coll_val = self.evaluate_expr_sync(collection)?;
4385                    crate::semantics::collections::index_set(&coll_val, &idx_val, new_val)?;
4386                }
4387                Ok(ControlFlow::Continue)
4388            }
4389
4390            Stmt::Splice { body } => {
4391                // Scope-transparent; see the async Splice handler.
4392                for s in body.iter() {
4393                    let flow = self.execute_stmt_sync(s)?;
4394                    if !matches!(flow, ControlFlow::Continue) {
4395                        return Ok(flow);
4396                    }
4397                }
4398                Ok(ControlFlow::Continue)
4399            }
4400
4401            Stmt::Inspect { target, arms, .. } => {
4402                let target_val = self.evaluate_expr_sync(target)?;
4403                self.execute_inspect_sync(&target_val, arms)
4404            }
4405
4406            Stmt::Zone { name, body, .. } => {
4407                self.push_scope();
4408                self.define(*name, RuntimeValue::Nothing);
4409                let result = self.execute_block_sync(body);
4410                self.pop_scope();
4411                result?;
4412                Ok(ControlFlow::Continue)
4413            }
4414
4415            Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
4416                for task in tasks.iter() {
4417                    self.execute_stmt_sync(task)?;
4418                }
4419                Ok(ControlFlow::Continue)
4420            }
4421
4422            Stmt::Assert { .. } | Stmt::Trust { .. } => {
4423                Ok(ControlFlow::Continue)
4424            }
4425
4426            Stmt::RuntimeAssert { condition, .. } => {
4427                let val = self.evaluate_expr_sync(condition)?;
4428                if !val.is_truthy() {
4429                    return Err("Assertion failed".to_string());
4430                }
4431                Ok(ControlFlow::Continue)
4432            }
4433
4434            Stmt::Give { object, recipient } => {
4435                let obj_val = self.evaluate_expr_sync(object)?;
4436                if let Expr::Identifier(sym) = recipient {
4437                    self.call_function_with_values_sync(*sym, vec![obj_val])?;
4438                }
4439                Ok(ControlFlow::Continue)
4440            }
4441
4442            Stmt::Show { object, recipient } => {
4443                let obj_val = self.evaluate_expr_sync(object)?;
4444                if let Expr::Identifier(sym) = recipient {
4445                    let name = self.ctx.interner.resolve(*sym);
4446                    if name == "show" {
4447                        self.emit_output(obj_val.to_display_string());
4448                    } else {
4449                        self.call_function_with_values_sync(*sym, vec![obj_val])?;
4450                    }
4451                }
4452                Ok(ControlFlow::Continue)
4453            }
4454
4455            // Async-only operations — unreachable in sync path (checked by needs_async)
4456            Stmt::ReadFrom { var, source } => {
4457                match source {
4458                    ReadSource::Console => {
4459                        self.define(*var, RuntimeValue::Text(Rc::new(String::new())));
4460                        Ok(ControlFlow::Continue)
4461                    }
4462                    ReadSource::File(_) => {
4463                        Err("File read requires async execution path".to_string())
4464                    }
4465                }
4466            }
4467
4468            Stmt::WriteFile { .. } => {
4469                Err("File write requires async execution path".to_string())
4470            }
4471
4472            Stmt::Spawn { name, .. } => {
4473                self.define(*name, RuntimeValue::Nothing);
4474                Ok(ControlFlow::Continue)
4475            }
4476
4477            Stmt::SendMessage { .. } => {
4478                Err("Send (peer messaging) requires the async execution path".to_string())
4479            }
4480            Stmt::StreamMessage { .. } => {
4481                Err("Stream (batch peer messaging) requires the async execution path".to_string())
4482            }
4483
4484            Stmt::AwaitMessage { .. } => {
4485                Err("Await (peer messaging) requires the async execution path".to_string())
4486            }
4487
4488            Stmt::MergeCrdt { source, target } => {
4489                let source_val = self.evaluate_expr_sync(source)?;
4490                let source_fields = match &source_val {
4491                    RuntimeValue::Struct(s) => s.fields.clone(),
4492                    _ => return Err("Merge source must be a struct".to_string()),
4493                };
4494
4495                if let Expr::Identifier(target_sym) = target {
4496                    let mut target_val = self.lookup(*target_sym)?.clone();
4497
4498                    if let RuntimeValue::Struct(ref mut s) = target_val {
4499                        for (field_name, source_field_val) in source_fields {
4500                            let current = s.fields.get(&field_name)
4501                                .cloned()
4502                                .unwrap_or(RuntimeValue::Int(0));
4503
4504                            let merged =
4505                                crate::semantics::arith::crdt_merge_field(&current, source_field_val);
4506                            s.fields.insert(field_name, merged);
4507                        }
4508                        self.assign(*target_sym, target_val)?;
4509                    } else {
4510                        return Err("Merge target must be a struct".to_string());
4511                    }
4512                } else {
4513                    return Err("Merge target must be an identifier".to_string());
4514                }
4515                Ok(ControlFlow::Continue)
4516            }
4517
4518            Stmt::IncreaseCrdt { object, field, amount } => {
4519                let amount_val = self.evaluate_expr_sync(amount)?;
4520                let amount_int = match amount_val {
4521                    RuntimeValue::Int(n) => n,
4522                    _ => return Err("CRDT increment amount must be an integer".to_string()),
4523                };
4524
4525                if let Expr::Identifier(obj_sym) = object {
4526                    let mut obj_val = self.lookup(*obj_sym)?.clone();
4527
4528                    if let RuntimeValue::Struct(ref mut s) = obj_val {
4529                        let field_name = self.ctx.interner.resolve(*field).to_string();
4530                        let current = s.fields.get(&field_name)
4531                            .cloned()
4532                            .unwrap_or(RuntimeValue::Int(0));
4533
4534                        let new_val =
4535                            crate::semantics::arith::crdt_counter_bump(current, amount_int, &field_name)?;
4536                        s.fields.insert(field_name, new_val);
4537                        self.assign(*obj_sym, obj_val)?;
4538                    } else {
4539                        return Err("Cannot increase field on non-struct value".to_string());
4540                    }
4541                } else {
4542                    return Err("IncreaseCrdt target must be an identifier".to_string());
4543                }
4544                Ok(ControlFlow::Continue)
4545            }
4546
4547            Stmt::DecreaseCrdt { object, field, amount } => {
4548                let amount_val = self.evaluate_expr_sync(amount)?;
4549                let amount_int = match amount_val {
4550                    RuntimeValue::Int(n) => n,
4551                    _ => return Err("CRDT decrement amount must be an integer".to_string()),
4552                };
4553
4554                if let Expr::Identifier(obj_sym) = object {
4555                    let mut obj_val = self.lookup(*obj_sym)?.clone();
4556
4557                    if let RuntimeValue::Struct(ref mut s) = obj_val {
4558                        let field_name = self.ctx.interner.resolve(*field).to_string();
4559                        let current = s.fields.get(&field_name)
4560                            .cloned()
4561                            .unwrap_or(RuntimeValue::Int(0));
4562
4563                        let new_val = crate::semantics::arith::crdt_counter_bump(
4564                            current,
4565                            amount_int.wrapping_neg(),
4566                            &field_name,
4567                        )?;
4568                        s.fields.insert(field_name, new_val);
4569                        self.assign(*obj_sym, obj_val)?;
4570                    } else {
4571                        return Err("Cannot decrease field on non-struct value".to_string());
4572                    }
4573                } else {
4574                    return Err("DecreaseCrdt target must be an identifier".to_string());
4575                }
4576                Ok(ControlFlow::Continue)
4577            }
4578
4579            Stmt::AppendToSequence { sequence, value } => {
4580                let val = self.evaluate_expr_sync(value)?;
4581                let seq_val = self.evaluate_expr_sync(sequence)?;
4582                match &seq_val {
4583                    RuntimeValue::Crdt(rc) => rc.borrow_mut().append(&val)?,
4584                    RuntimeValue::List(_) => {
4585                        crate::semantics::collections::list_push(&seq_val, val)?
4586                    }
4587                    _ => return Err(format!("Cannot append to {}", seq_val.type_name())),
4588                }
4589                Ok(ControlFlow::Continue)
4590            }
4591
4592            Stmt::ResolveConflict { object, field, value } => {
4593                let val = self.evaluate_expr_sync(value)?;
4594                let obj_val = self.evaluate_expr_sync(object)?;
4595                let field_name = self.ctx.interner.resolve(*field);
4596                if let RuntimeValue::Struct(s) = &obj_val {
4597                    if let Some(RuntimeValue::Crdt(rc)) = s.fields.get(field_name) {
4598                        rc.borrow_mut().resolve(&val)?;
4599                        return Ok(ControlFlow::Continue);
4600                    }
4601                }
4602                if let Expr::Identifier(obj_sym) = object {
4603                    let mut owner = self.lookup(*obj_sym)?.clone();
4604                    if let RuntimeValue::Struct(ref mut s) = owner {
4605                        s.fields.insert(field_name.to_string(), val);
4606                        self.assign(*obj_sym, owner)?;
4607                        return Ok(ControlFlow::Continue);
4608                    }
4609                }
4610                Err("Resolve target must be a struct field".to_string())
4611            }
4612
4613            Stmt::Check { subject, predicate, is_capability, object, source_text, .. } => {
4614                let registry = match &self.ctx.policy_registry {
4615                    Some(r) => r,
4616                    None => return Err("Security Check requires policies. Use compiled Rust or add ## Policy block.".to_string()),
4617                };
4618
4619                let subj_val = self.lookup(*subject)?.clone();
4620                // The object is only consulted (and only looked up) for
4621                // capability checks.
4622                let obj_val = if *is_capability {
4623                    match object {
4624                        Some(obj_sym) => Some(self.lookup(*obj_sym)?.clone()),
4625                        None => None,
4626                    }
4627                } else {
4628                    None
4629                };
4630                crate::semantics::policy::check_policy(
4631                    registry,
4632                    self.ctx.interner,
4633                    &subj_val,
4634                    *predicate,
4635                    *is_capability,
4636                    obj_val.as_ref(),
4637                    source_text,
4638                )?;
4639                Ok(ControlFlow::Continue)
4640            }
4641
4642            Stmt::Listen { .. } | Stmt::ConnectTo { .. } => {
4643                Err("Networking (Connect/Listen) requires the async execution path".to_string())
4644            }
4645            // A PeerAgent handle is pure (just its canonical topic), so it works
4646            // outside the async path; the `Send`/`Await` that use it do not.
4647            Stmt::LetPeerAgent { var, address } => {
4648                let raw = self.evaluate_expr_sync(address)?.to_display_string();
4649                let topic = logicaffeine_system::addr::canonical_topic(&raw);
4650                self.define(*var, RuntimeValue::Peer(Rc::new(topic)));
4651                Ok(ControlFlow::Continue)
4652            }
4653            Stmt::Sleep { .. } => {
4654                Err("Sleep requires async execution path".to_string())
4655            }
4656            Stmt::Sync { .. } => {
4657                Err("Sync requires the async execution path".to_string())
4658            }
4659            Stmt::Mount { .. } => {
4660                Err("Mount requires async execution path".to_string())
4661            }
4662
4663            Stmt::LaunchTask { .. } |
4664            Stmt::LaunchTaskWithHandle { .. } |
4665            Stmt::CreatePipe { .. } |
4666            Stmt::SendPipe { .. } |
4667            Stmt::ReceivePipe { .. } |
4668            Stmt::TrySendPipe { .. } |
4669            Stmt::TryReceivePipe { .. } |
4670            Stmt::StopTask { .. } |
4671            Stmt::Select { .. } => {
4672                Err("Go-like concurrency (Launch, Pipe, Select) is only supported in compiled mode".to_string())
4673            }
4674
4675            Stmt::Escape { .. } => {
4676                Err(
4677                    "Escape blocks contain raw Rust code and cannot be interpreted. \
4678                     Use `largo build` or `largo run` to compile and run this program."
4679                    .to_string()
4680                )
4681            }
4682
4683            Stmt::Require { .. } => {
4684                Ok(ControlFlow::Continue)
4685            }
4686
4687            Stmt::Theorem(_) | Stmt::Definition(_) | Stmt::Axiom(_) | Stmt::Theory(_) => {
4688                Ok(ControlFlow::Continue)
4689            }
4690        }
4691    }
4692
4693    fn execute_block_sync(&mut self, block: Block<'a>) -> Result<ControlFlow, String> {
4694        self.push_scope();
4695        for stmt in block.iter() {
4696            match self.execute_stmt_sync(stmt)? {
4697                ControlFlow::Continue => {}
4698                flow => {
4699                    self.pop_scope();
4700                    return Ok(flow);
4701                }
4702            }
4703        }
4704        self.pop_scope();
4705        Ok(ControlFlow::Continue)
4706    }
4707
4708    fn execute_inspect_sync(&mut self, target: &RuntimeValue, arms: &[MatchArm<'a>]) -> Result<ControlFlow, String> {
4709        for arm in arms {
4710            if arm.variant.is_none() {
4711                let flow = self.execute_block_sync(arm.body)?;
4712                return Ok(flow);
4713            }
4714
4715            match target {
4716                RuntimeValue::Struct(s) => {
4717                    if let Some(variant) = arm.variant {
4718                        let variant_name = self.ctx.interner.resolve(variant);
4719                        if s.type_name == variant_name {
4720                            self.push_scope();
4721                            for (field_name, binding_name) in &arm.bindings {
4722                                let field_str = self.ctx.interner.resolve(*field_name);
4723                                if let Some(val) = s.fields.get(field_str) {
4724                                    self.define(*binding_name, val.clone());
4725                                }
4726                            }
4727                            let result = self.execute_block_sync(arm.body);
4728                            self.pop_scope();
4729                            let flow = result?;
4730                            return Ok(flow);
4731                        }
4732                    }
4733                }
4734
4735                RuntimeValue::Inductive(ind) => {
4736                    if let Some(variant) = arm.variant {
4737                        let variant_name = self.ctx.interner.resolve(variant);
4738                        if ind.constructor == variant_name {
4739                            self.push_scope();
4740                            for (i, (_, binding_name)) in arm.bindings.iter().enumerate() {
4741                                if i < ind.args.len() {
4742                                    self.define(*binding_name, ind.args[i].clone());
4743                                }
4744                            }
4745                            let result = self.execute_block_sync(arm.body);
4746                            self.pop_scope();
4747                            let flow = result?;
4748                            return Ok(flow);
4749                        }
4750                    }
4751                }
4752
4753                _ => {}
4754            }
4755        }
4756        Err(self.inspect_unhandled(target))
4757    }
4758
4759    fn evaluate_expr_sync(&mut self, expr: &Expr<'a>) -> Result<RuntimeValue, String> {
4760        match expr {
4761            Expr::Literal(lit) => self.evaluate_literal(lit),
4762
4763            Expr::Identifier(sym) => {
4764                let name = self.ctx.interner.resolve(*sym);
4765                // Handle temporal builtins (the NAME wins, even when shadowed)
4766                match name {
4767                    "today" => {
4768                        return Ok(crate::semantics::temporal::today());
4769                    }
4770                    "now" => {
4771                        return Ok(crate::semantics::temporal::now());
4772                    }
4773                    _ => {}
4774                }
4775                self.lookup(*sym).cloned()
4776            }
4777
4778            Expr::BinaryOp { op, left, right } => {
4779                match op {
4780                    BinaryOpKind::And => {
4781                        let left_val = self.evaluate_expr_sync(left)?;
4782                        if !left_val.is_truthy() {
4783                            return Ok(RuntimeValue::Bool(false));
4784                        }
4785                        let right_val = self.evaluate_expr_sync(right)?;
4786                        Ok(RuntimeValue::Bool(right_val.is_truthy()))
4787                    }
4788                    BinaryOpKind::Or => {
4789                        let left_val = self.evaluate_expr_sync(left)?;
4790                        if left_val.is_truthy() {
4791                            return Ok(RuntimeValue::Bool(true));
4792                        }
4793                        let right_val = self.evaluate_expr_sync(right)?;
4794                        Ok(RuntimeValue::Bool(right_val.is_truthy()))
4795                    }
4796                    _ => {
4797                        let left_val = self.evaluate_expr_sync(left)?;
4798                        let right_val = self.evaluate_expr_sync(right)?;
4799                        self.apply_binary_op(*op, left_val, right_val)
4800                    }
4801                }
4802            }
4803
4804            Expr::Call { function, args } => {
4805                self.call_function_sync(*function, args)
4806            }
4807
4808            Expr::Index { collection, index } => {
4809                let coll_val = self.evaluate_expr_sync(collection)?;
4810                let idx_val = self.evaluate_expr_sync(index)?;
4811                crate::semantics::collections::index_get(&coll_val, &idx_val)
4812            }
4813
4814            Expr::Slice { collection, start, end } => {
4815                let coll_val = self.evaluate_expr_sync(collection)?;
4816                let start_val = self.evaluate_expr_sync(start)?;
4817                let end_val = self.evaluate_expr_sync(end)?;
4818                crate::semantics::collections::slice(&coll_val, &start_val, &end_val)
4819            }
4820
4821            Expr::Copy { expr: inner } => {
4822                let val = self.evaluate_expr_sync(inner)?;
4823                Ok(val.deep_clone())
4824            }
4825
4826            Expr::Give { value } => {
4827                self.evaluate_expr_sync(value)
4828            }
4829
4830            Expr::Length { collection } => {
4831                let coll_val = self.evaluate_expr_sync(collection)?;
4832                crate::semantics::collections::length_of(&coll_val)
4833            }
4834
4835            Expr::Contains { collection, value } => {
4836                let coll_val = self.evaluate_expr_sync(collection)?;
4837                let val = self.evaluate_expr_sync(value)?;
4838                crate::semantics::collections::contains(&coll_val, &val)
4839            }
4840
4841            Expr::Union { left, right } => {
4842                let left_val = self.evaluate_expr_sync(left)?;
4843                let right_val = self.evaluate_expr_sync(right)?;
4844                crate::semantics::collections::union(&left_val, &right_val)
4845            }
4846
4847            Expr::Intersection { left, right } => {
4848                let left_val = self.evaluate_expr_sync(left)?;
4849                let right_val = self.evaluate_expr_sync(right)?;
4850                crate::semantics::collections::intersection(&left_val, &right_val)
4851            }
4852
4853            Expr::List(items) => {
4854                let mut values = Vec::with_capacity(items.len());
4855                for e in items.iter() {
4856                    values.push(self.evaluate_expr_sync(e)?);
4857                }
4858                Ok(RuntimeValue::List(Rc::new(RefCell::new(ListRepr::from_values(values)))))
4859            }
4860
4861            Expr::Tuple(items) => {
4862                let mut values = Vec::with_capacity(items.len());
4863                for e in items.iter() {
4864                    values.push(self.evaluate_expr_sync(e)?);
4865                }
4866                Ok(RuntimeValue::Tuple(Rc::new(values)))
4867            }
4868
4869            Expr::Range { start, end } => {
4870                let start_val = self.evaluate_expr_sync(start)?;
4871                let end_val = self.evaluate_expr_sync(end)?;
4872                crate::semantics::collections::range(&start_val, &end_val)
4873            }
4874
4875            Expr::FieldAccess { object, field } => {
4876                let obj_val = self.evaluate_expr_sync(object)?;
4877                match &obj_val {
4878                    RuntimeValue::Struct(s) => {
4879                        let field_name = self.ctx.interner.resolve(*field);
4880                        s.fields.get(field_name).cloned()
4881                            .ok_or_else(|| format!("Field '{}' not found", field_name))
4882                    }
4883                    _ => Err(format!("Cannot access field on {}", obj_val.type_name())),
4884                }
4885            }
4886
4887            Expr::New { type_name, init_fields, .. } => {
4888                let name = self.ctx.interner.resolve(*type_name).to_string();
4889
4890                if name == "Seq" || name == "List" {
4891                    return Ok(RuntimeValue::List(Rc::new(RefCell::new(ListRepr::Ints(Vec::new())))));
4892                }
4893
4894                if name == "Set" || name == "HashSet" {
4895                    return Ok(RuntimeValue::Set(Rc::new(RefCell::new(vec![]))));
4896                }
4897
4898                if name == "Map" || name == "HashMap" {
4899                    return Ok(RuntimeValue::Map(Rc::new(RefCell::new(MapStorage::default()))));
4900                }
4901
4902                let mut fields = HashMap::new();
4903                for (field_sym, field_expr) in init_fields {
4904                    let field_name = self.ctx.interner.resolve(*field_sym).to_string();
4905                    let field_val = self.evaluate_expr_sync(field_expr)?;
4906                    fields.insert(field_name, field_val);
4907                }
4908
4909                if let Some(def) = self.ctx.struct_defs.get(type_name) {
4910                    for (field_sym, type_sym, _) in def {
4911                        let field_name = self.ctx.interner.resolve(*field_sym).to_string();
4912                        if !fields.contains_key(&field_name) {
4913                            let type_name_str = self.ctx.interner.resolve(*type_sym).to_string();
4914                            let default = match type_name_str.as_str() {
4915                                "Int" => RuntimeValue::Int(0),
4916                                "Float" => RuntimeValue::Float(0.0),
4917                                "Bool" => RuntimeValue::Bool(false),
4918                                "Text" | "String" => RuntimeValue::Text(Rc::new(String::new())),
4919                                "Char" => RuntimeValue::Char('\0'),
4920                                "Byte" => RuntimeValue::Int(0),
4921                                "Seq" | "List" => RuntimeValue::List(Rc::new(RefCell::new(ListRepr::Ints(Vec::new())))),
4922                                "Set" | "HashSet" => RuntimeValue::Set(Rc::new(RefCell::new(vec![]))),
4923                                "Map" | "HashMap" => RuntimeValue::Map(Rc::new(RefCell::new(MapStorage::default()))),
4924                                // A `Shared` struct's CRDT fields default to an empty live
4925                                // CRDT, mirroring the compiled tier's `ORSet`/`RGA` field.
4926                                "SharedSet" | "ORSet" | "SharedSet_AddWins" =>
4927                                    RuntimeValue::Crdt(Rc::new(RefCell::new(
4928                                        crate::semantics::crdt::CrdtValue::new_set(
4929                                            crate::semantics::crdt::next_replica_id())))),
4930                                "SharedSet_RemoveWins" =>
4931                                    RuntimeValue::Crdt(Rc::new(RefCell::new(
4932                                        crate::semantics::crdt::CrdtValue::new_set_remove_wins(
4933                                            crate::semantics::crdt::next_replica_id())))),
4934                                "SharedSequence" | "RGA" | "SharedSequence_YATA" | "CollaborativeSequence" =>
4935                                    RuntimeValue::Crdt(Rc::new(RefCell::new(
4936                                        crate::semantics::crdt::CrdtValue::new_seq(
4937                                            crate::semantics::crdt::next_replica_id())))),
4938                                _ => RuntimeValue::Nothing,
4939                            };
4940                            fields.insert(field_name, default);
4941                        }
4942                    }
4943                }
4944
4945                Ok(RuntimeValue::Struct(Box::new(StructValue { type_name: name, fields })))
4946            }
4947
4948            Expr::NewVariant { enum_name, variant, fields } => {
4949                let inductive_type = self.ctx.interner.resolve(*enum_name).to_string();
4950                let constructor = self.ctx.interner.resolve(*variant).to_string();
4951
4952                let mut args = Vec::new();
4953                for (_, field_expr) in fields {
4954                    let field_val = self.evaluate_expr_sync(field_expr)?;
4955                    args.push(field_val);
4956                }
4957
4958                Ok(RuntimeValue::Inductive(Box::new(InductiveValue {
4959                    inductive_type,
4960                    constructor,
4961                    args,
4962                })))
4963            }
4964
4965            Expr::ManifestOf { .. } => {
4966                Ok(RuntimeValue::List(Rc::new(RefCell::new(ListRepr::Ints(Vec::new())))))
4967            }
4968
4969            Expr::ChunkAt { .. } => {
4970                Ok(RuntimeValue::Nothing)
4971            }
4972
4973            Expr::WithCapacity { value, .. } => {
4974                self.evaluate_expr_sync(value)
4975            }
4976
4977            Expr::OptionSome { value } => {
4978                self.evaluate_expr_sync(value)
4979            }
4980
4981            Expr::OptionNone => {
4982                Ok(RuntimeValue::Nothing)
4983            }
4984
4985            Expr::Not { operand } => {
4986                let val = self.evaluate_expr_sync(operand)?;
4987                crate::semantics::arith::not_value(val)
4988            }
4989
4990            Expr::InterpolatedString(parts) => {
4991                let mut result = String::new();
4992                for part in parts {
4993                    match part {
4994                        crate::ast::stmt::StringPart::Literal(sym) => {
4995                            result.push_str(self.ctx.interner.resolve(*sym));
4996                        }
4997                        crate::ast::stmt::StringPart::Expr { value, format_spec, debug } => {
4998                            let val = self.evaluate_expr_sync(value)?;
4999                            if *debug {
5000                                let prefix = match value {
5001                                    Expr::Identifier(sym) => self.ctx.interner.resolve(*sym).to_string(),
5002                                    _ => "expr".to_string(),
5003                                };
5004                                result.push_str(&prefix);
5005                                result.push('=');
5006                            }
5007                            if let Some(spec_sym) = format_spec {
5008                                let spec = self.ctx.interner.resolve(*spec_sym);
5009                                result.push_str(&apply_format_spec(&val, spec));
5010                            } else {
5011                                result.push_str(&val.to_display_string());
5012                            }
5013                        }
5014                    }
5015                }
5016                Ok(RuntimeValue::Text(Rc::new(result)))
5017            }
5018
5019            Expr::Escape { .. } => {
5020                Err("Escape expressions contain raw Rust code and cannot be interpreted. \
5021                     Use `largo build` or `largo run` to compile and run this program.".to_string())
5022            }
5023
5024            Expr::Closure { params, body, .. } => {
5025                let free_vars = self.collect_free_vars_in_closure(params, body);
5026                let mut captured_env = HashMap::new();
5027                for sym in &free_vars {
5028                    if let Some(val) = self.task.env.lookup(*sym) {
5029                        captured_env.insert(*sym, val.deep_clone());
5030                    }
5031                }
5032
5033                let body_index = self.ctx.closure_bodies.len();
5034                match body {
5035                    ClosureBody::Expression(expr) => {
5036                        self.ctx.closure_bodies.push(ClosureBodyRef::Expression(expr));
5037                    }
5038                    ClosureBody::Block(block) => {
5039                        self.ctx.closure_bodies.push(ClosureBodyRef::Block(block));
5040                    }
5041                }
5042
5043                let param_names: Vec<Symbol> = params.iter().map(|(name, _)| *name).collect();
5044
5045                Ok(RuntimeValue::Function(Box::new(ClosureValue {
5046                    body_index,
5047                    captured_env,
5048                    param_names,
5049                    generated: None,
5050                })))
5051            }
5052
5053            Expr::CallExpr { callee, args } => {
5054                let callee_val = self.evaluate_expr_sync(callee)?;
5055                if let RuntimeValue::Function(closure) = callee_val {
5056                    let mut arg_values = Vec::with_capacity(args.len());
5057                    for arg in args.iter() {
5058                        arg_values.push(self.evaluate_expr_sync(arg)?);
5059                    }
5060                    self.call_closure_value_sync(&closure, arg_values)
5061                } else {
5062                    Err(format!("Cannot call value of type {}", callee_val.type_name()))
5063                }
5064            }
5065        }
5066    }
5067
5068    /// If `expr` is a direct self-tail-call — `self(args)` of the function the
5069    /// SYNC path is currently executing, with matching arity and not inside a
5070    /// `Repeat` — return its argument expressions for `call_function_sync` to
5071    /// evaluate and loop on. `None` leaves the `Return` an ordinary one.
5072    fn self_tail_call_args_sync(&self, expr: &'a Expr<'a>) -> Option<&'a [&'a Expr<'a>]> {
5073        if self.task.repeat_depth_sync != 0 {
5074            return None;
5075        }
5076        let cur = self.task.tco_fn_sync?;
5077        let param_count = self.ctx.functions.get(&cur)?.params.len();
5078        crate::tail_call::direct_self_tail_args(expr, cur, param_count)
5079    }
5080
5081    /// ASYNC twin of [`Self::self_tail_call_args_sync`].
5082    fn self_tail_call_args_async(&self, expr: &'a Expr<'a>) -> Option<&'a [&'a Expr<'a>]> {
5083        if self.task.repeat_depth_async != 0 {
5084            return None;
5085        }
5086        let cur = self.task.tco_fn_async?;
5087        let param_count = self.ctx.functions.get(&cur)?.params.len();
5088        crate::tail_call::direct_self_tail_args(expr, cur, param_count)
5089    }
5090
5091    fn call_function_sync(&mut self, function: Symbol, args: &[&Expr<'a>]) -> Result<RuntimeValue, String> {
5092        // Built-in functions — Symbol comparison (integer) instead of string matching
5093        let func_sym = Some(function);
5094        if func_sym == self.ctx.sym_show {
5095            for arg in args {
5096                let val = self.evaluate_expr_sync(arg)?;
5097                self.emit_output(val.to_display_string());
5098            }
5099            return Ok(RuntimeValue::Nothing);
5100        } else if func_sym == self.ctx.sym_args {
5101            // `args()` system native: the stored argv as a `Seq of Text`,
5102            // mirroring the compiled binary's `env::args()`. Must match the
5103            // async path AND the VM (the shadow oracle asserts VM ≡ tree-walker).
5104            let items: Vec<RuntimeValue> = self
5105                .ctx
5106                .program_args
5107                .iter()
5108                .map(|s| RuntimeValue::Text(Rc::new(s.clone())))
5109                .collect();
5110            return Ok(RuntimeValue::List(Rc::new(RefCell::new(ListRepr::from_values(items)))));
5111        } else if let Some(id) = self.builtin_id(function) {
5112            // Arity is checked BEFORE evaluating arguments (kernel rule).
5113            crate::semantics::builtins::check_arity(id, args.len())?;
5114            // `format` reads only its first argument; preserve its laziness.
5115            let vals = if id == crate::semantics::builtins::BuiltinId::Format {
5116                match args.first() {
5117                    Some(a) => vec![self.evaluate_expr_sync(a)?],
5118                    None => Vec::new(),
5119                }
5120            } else {
5121                let mut v = Vec::with_capacity(args.len());
5122                for arg in args {
5123                    v.push(self.evaluate_expr_sync(arg)?);
5124                }
5125                v
5126            };
5127            return crate::semantics::builtins::call_builtin(id, vals);
5128        }
5129
5130        // User-defined function lookup — extract metadata without cloning params
5131        if let Some(func) = self.ctx.functions.get(&function) {
5132            let param_count = func.params.len();
5133            let body = func.body;
5134
5135            if args.len() != param_count {
5136                return Err(format!(
5137                    "Function {} expects {} arguments, got {}",
5138                    self.ctx.interner.resolve(function),
5139                    param_count,
5140                    args.len()
5141                ));
5142            }
5143
5144            let mut arg_values = Vec::with_capacity(param_count);
5145            for arg in args {
5146                arg_values.push(self.evaluate_expr_sync(arg)?);
5147            }
5148
5149            if self.task.call_depth >= crate::semantics::MAX_CALL_DEPTH {
5150            return Err(crate::semantics::CALL_DEPTH_ERR.to_string());
5151        }
5152        self.task.call_depth += 1;
5153        self.task.env.push_frame();
5154            for i in 0..param_count {
5155                let param_name = self.ctx.functions[&function].params[i].0;
5156                self.task.env.define(param_name, std::mem::replace(&mut arg_values[i], RuntimeValue::Nothing));
5157            }
5158
5159            // TCO: while executing THIS function's body, a self-tail-call is a
5160            // loop-back (reassign params + restart the body) rather than a real
5161            // recursive call. `tco_fn_sync`/`repeat_depth_sync` are per-activation,
5162            // so save the caller's and reset for this body.
5163            let prev_tco = self.task.tco_fn_sync.replace(function);
5164            let prev_repeat = std::mem::replace(&mut self.task.repeat_depth_sync, 0);
5165            let mut return_value = RuntimeValue::Nothing;
5166            let mut body_err = None;
5167            'tco: loop {
5168                self.task.pending_tail_call = None;
5169                let mut idx = 0;
5170                while idx < body.len() {
5171                    // Top-level `Set/Let x to self(args); Return x` pair — a tail
5172                    // call. (A direct `Return self(args)` at any depth is caught
5173                    // in execute_stmt_sync's Return arm.)
5174                    if idx + 1 < body.len() {
5175                        if let Some(call_args) = crate::tail_call::tail_pair_args(
5176                            &body[idx],
5177                            &body[idx + 1],
5178                            function,
5179                            param_count,
5180                        ) {
5181                            let mut vals = Vec::with_capacity(call_args.len());
5182                            let mut perr = None;
5183                            for a in call_args {
5184                                match self.evaluate_expr_sync(a) {
5185                                    Ok(v) => vals.push(v),
5186                                    Err(e) => {
5187                                        perr = Some(e);
5188                                        break;
5189                                    }
5190                                }
5191                            }
5192                            match perr {
5193                                Some(e) => body_err = Some(e),
5194                                None => self.task.pending_tail_call = Some(vals),
5195                            }
5196                            break;
5197                        }
5198                    }
5199                    match self.execute_stmt_sync(&body[idx]) {
5200                        Ok(ControlFlow::Return(val)) => {
5201                            return_value = val;
5202                            break;
5203                        }
5204                        Ok(ControlFlow::Break) => break,
5205                        Ok(ControlFlow::Continue) => {}
5206                        Err(e) => {
5207                            body_err = Some(e);
5208                            break;
5209                        }
5210                    }
5211                    idx += 1;
5212                }
5213                if body_err.is_some() {
5214                    break 'tco;
5215                }
5216                match self.task.pending_tail_call.take() {
5217                    Some(new_args) => {
5218                        // Loop-back: a fresh frame (no stale locals) with the
5219                        // reassigned parameters — constant stack, no depth bump.
5220                        self.task.env.pop_frame();
5221                        self.task.env.push_frame();
5222                        for (i, v) in new_args.into_iter().enumerate() {
5223                            let param_name = self.ctx.functions[&function].params[i].0;
5224                            self.task.env.define(param_name, v);
5225                        }
5226                        continue 'tco;
5227                    }
5228                    None => break 'tco,
5229                }
5230            }
5231            self.task.repeat_depth_sync = prev_repeat;
5232            self.task.tco_fn_sync = prev_tco;
5233
5234            self.task.env.pop_frame();
5235        self.task.call_depth -= 1;
5236            match body_err {
5237                Some(e) => Err(e),
5238                None => Ok(return_value),
5239            }
5240        } else {
5241            // Fallback: check if the function name is a variable holding a closure
5242            let maybe_closure = self.task.env.lookup(function)
5243                .and_then(|v| if let RuntimeValue::Function(c) = v { Some((**c).clone()) } else { None });
5244
5245            if let Some(closure) = maybe_closure {
5246                let mut arg_values = Vec::with_capacity(args.len());
5247                for arg in args {
5248                    arg_values.push(self.evaluate_expr_sync(arg)?);
5249                }
5250                self.call_closure_value_sync(&closure, arg_values)
5251            } else {
5252                Err(format!("Unknown function: {}", self.ctx.interner.resolve(function)))
5253            }
5254        }
5255    }
5256
5257    fn call_function_with_values_sync(&mut self, function: Symbol, mut args: Vec<RuntimeValue>) -> Result<RuntimeValue, String> {
5258        // Handle built-in "show" via Symbol comparison
5259        if Some(function) == self.ctx.sym_show {
5260            for val in args {
5261                self.emit_output(val.to_display_string());
5262            }
5263            return Ok(RuntimeValue::Nothing);
5264        }
5265
5266        if let Some(func) = self.ctx.functions.get(&function) {
5267            let param_count = func.params.len();
5268            let body = func.body;
5269
5270            if args.len() != param_count {
5271                return Err(format!(
5272                    "Function {} expects {} arguments, got {}",
5273                    self.ctx.interner.resolve(function), param_count, args.len()
5274                ));
5275            }
5276
5277            if self.task.call_depth >= crate::semantics::MAX_CALL_DEPTH {
5278            return Err(crate::semantics::CALL_DEPTH_ERR.to_string());
5279        }
5280        self.task.call_depth += 1;
5281        self.task.env.push_frame();
5282            for i in 0..param_count {
5283                let param_name = self.ctx.functions[&function].params[i].0;
5284                self.task.env.define(param_name, std::mem::replace(&mut args[i], RuntimeValue::Nothing));
5285            }
5286
5287            // TCO: while executing THIS function's body, a self-tail-call is a
5288            // loop-back (reassign params + restart the body) rather than a real
5289            // recursive call. `tco_fn_sync`/`repeat_depth_sync` are per-activation,
5290            // so save the caller's and reset for this body.
5291            let prev_tco = self.task.tco_fn_sync.replace(function);
5292            let prev_repeat = std::mem::replace(&mut self.task.repeat_depth_sync, 0);
5293            let mut return_value = RuntimeValue::Nothing;
5294            let mut body_err = None;
5295            'tco: loop {
5296                self.task.pending_tail_call = None;
5297                let mut idx = 0;
5298                while idx < body.len() {
5299                    // Top-level `Set/Let x to self(args); Return x` pair — a tail
5300                    // call. (A direct `Return self(args)` at any depth is caught
5301                    // in execute_stmt_sync's Return arm.)
5302                    if idx + 1 < body.len() {
5303                        if let Some(call_args) = crate::tail_call::tail_pair_args(
5304                            &body[idx],
5305                            &body[idx + 1],
5306                            function,
5307                            param_count,
5308                        ) {
5309                            let mut vals = Vec::with_capacity(call_args.len());
5310                            let mut perr = None;
5311                            for a in call_args {
5312                                match self.evaluate_expr_sync(a) {
5313                                    Ok(v) => vals.push(v),
5314                                    Err(e) => {
5315                                        perr = Some(e);
5316                                        break;
5317                                    }
5318                                }
5319                            }
5320                            match perr {
5321                                Some(e) => body_err = Some(e),
5322                                None => self.task.pending_tail_call = Some(vals),
5323                            }
5324                            break;
5325                        }
5326                    }
5327                    match self.execute_stmt_sync(&body[idx]) {
5328                        Ok(ControlFlow::Return(val)) => {
5329                            return_value = val;
5330                            break;
5331                        }
5332                        Ok(ControlFlow::Break) => break,
5333                        Ok(ControlFlow::Continue) => {}
5334                        Err(e) => {
5335                            body_err = Some(e);
5336                            break;
5337                        }
5338                    }
5339                    idx += 1;
5340                }
5341                if body_err.is_some() {
5342                    break 'tco;
5343                }
5344                match self.task.pending_tail_call.take() {
5345                    Some(new_args) => {
5346                        // Loop-back: a fresh frame (no stale locals) with the
5347                        // reassigned parameters — constant stack, no depth bump.
5348                        self.task.env.pop_frame();
5349                        self.task.env.push_frame();
5350                        for (i, v) in new_args.into_iter().enumerate() {
5351                            let param_name = self.ctx.functions[&function].params[i].0;
5352                            self.task.env.define(param_name, v);
5353                        }
5354                        continue 'tco;
5355                    }
5356                    None => break 'tco,
5357                }
5358            }
5359            self.task.repeat_depth_sync = prev_repeat;
5360            self.task.tco_fn_sync = prev_tco;
5361
5362            self.task.env.pop_frame();
5363        self.task.call_depth -= 1;
5364            match body_err {
5365                Some(e) => Err(e),
5366                None => Ok(return_value),
5367            }
5368        } else {
5369            let maybe_closure = self.task.env.lookup(function)
5370                .and_then(|v| if let RuntimeValue::Function(c) = v { Some((**c).clone()) } else { None });
5371
5372            if let Some(closure) = maybe_closure {
5373                self.call_closure_value_sync(&closure, args)
5374            } else {
5375                Err(format!("Unknown function: {}", self.ctx.interner.resolve(function)))
5376            }
5377        }
5378    }
5379
5380    // =========================================================================
5381    // Closure support: free variable collection and closure invocation
5382    // =========================================================================
5383
5384    /// Collect free variable symbols from a closure body.
5385    /// Returns all Identifier symbols referenced in the body that are not parameter names.
5386    /// Shared with the bytecode VM's compiler — both engines MUST agree on the
5387    /// capture set, so there is exactly one implementation.
5388    pub(crate) fn collect_free_vars_in_closure(
5389        &self,
5390        params: &[(Symbol, &TypeExpr<'a>)],
5391        body: &ClosureBody<'a>,
5392    ) -> Vec<Symbol> {
5393        Self::free_vars_in_closure(params, body)
5394    }
5395
5396    /// Static form of [`Self::collect_free_vars_in_closure`] (the VM compiler
5397    /// has no interpreter instance).
5398    pub(crate) fn free_vars_in_closure(
5399        params: &[(Symbol, &TypeExpr<'a>)],
5400        body: &ClosureBody<'a>,
5401    ) -> Vec<Symbol> {
5402        let param_set: std::collections::HashSet<Symbol> = params.iter().map(|(s, _)| *s).collect();
5403        let mut out = Vec::new();
5404        let mut seen = std::collections::HashSet::new();
5405
5406        match body {
5407            ClosureBody::Expression(expr) => {
5408                Self::collect_symbols_from_expr(expr, &param_set, &mut out, &mut seen);
5409            }
5410            ClosureBody::Block(block) => {
5411                Self::collect_symbols_from_block(block, &param_set, &mut out, &mut seen);
5412            }
5413        }
5414
5415        out
5416    }
5417
5418    fn collect_symbols_from_expr(
5419        expr: &Expr<'a>,
5420        exclude: &std::collections::HashSet<Symbol>,
5421        out: &mut Vec<Symbol>,
5422        seen: &mut std::collections::HashSet<Symbol>,
5423    ) {
5424        match expr {
5425            Expr::Identifier(sym) => {
5426                if !exclude.contains(sym) && seen.insert(*sym) {
5427                    out.push(*sym);
5428                }
5429            }
5430            Expr::Literal(_) | Expr::OptionNone | Expr::Escape { .. } => {}
5431            Expr::BinaryOp { left, right, .. } => {
5432                Self::collect_symbols_from_expr(left, exclude, out, seen);
5433                Self::collect_symbols_from_expr(right, exclude, out, seen);
5434            }
5435            Expr::Call { function, args } => {
5436                if !exclude.contains(function) && seen.insert(*function) {
5437                    out.push(*function);
5438                }
5439                for arg in args {
5440                    Self::collect_symbols_from_expr(arg, exclude, out, seen);
5441                }
5442            }
5443            Expr::FieldAccess { object, .. } => {
5444                Self::collect_symbols_from_expr(object, exclude, out, seen);
5445            }
5446            Expr::Index { collection, index } => {
5447                Self::collect_symbols_from_expr(collection, exclude, out, seen);
5448                Self::collect_symbols_from_expr(index, exclude, out, seen);
5449            }
5450            Expr::Slice { collection, start, end } => {
5451                Self::collect_symbols_from_expr(collection, exclude, out, seen);
5452                Self::collect_symbols_from_expr(start, exclude, out, seen);
5453                Self::collect_symbols_from_expr(end, exclude, out, seen);
5454            }
5455            Expr::Copy { expr: e } | Expr::Give { value: e } | Expr::Length { collection: e }
5456            | Expr::Not { operand: e } => {
5457                Self::collect_symbols_from_expr(e, exclude, out, seen);
5458            }
5459            Expr::List(items) | Expr::Tuple(items) => {
5460                for item in items {
5461                    Self::collect_symbols_from_expr(item, exclude, out, seen);
5462                }
5463            }
5464            Expr::Range { start, end } => {
5465                Self::collect_symbols_from_expr(start, exclude, out, seen);
5466                Self::collect_symbols_from_expr(end, exclude, out, seen);
5467            }
5468            Expr::New { init_fields, .. } => {
5469                for (_, e) in init_fields {
5470                    Self::collect_symbols_from_expr(e, exclude, out, seen);
5471                }
5472            }
5473            Expr::NewVariant { fields, .. } => {
5474                for (_, e) in fields {
5475                    Self::collect_symbols_from_expr(e, exclude, out, seen);
5476                }
5477            }
5478            Expr::Contains { collection, value } | Expr::Union { left: collection, right: value }
5479            | Expr::Intersection { left: collection, right: value } => {
5480                Self::collect_symbols_from_expr(collection, exclude, out, seen);
5481                Self::collect_symbols_from_expr(value, exclude, out, seen);
5482            }
5483            Expr::ManifestOf { zone } | Expr::OptionSome { value: zone } => {
5484                Self::collect_symbols_from_expr(zone, exclude, out, seen);
5485            }
5486            Expr::ChunkAt { index, zone } | Expr::WithCapacity { value: index, capacity: zone } => {
5487                Self::collect_symbols_from_expr(index, exclude, out, seen);
5488                Self::collect_symbols_from_expr(zone, exclude, out, seen);
5489            }
5490            Expr::Closure { params: inner_params, body: inner_body, .. } => {
5491                // Nested closure: exclude inner params too
5492                let mut inner_exclude = exclude.clone();
5493                for (s, _) in inner_params {
5494                    inner_exclude.insert(*s);
5495                }
5496                match inner_body {
5497                    ClosureBody::Expression(e) => {
5498                        Self::collect_symbols_from_expr(e, &inner_exclude, out, seen);
5499                    }
5500                    ClosureBody::Block(b) => {
5501                        Self::collect_symbols_from_block(b, &inner_exclude, out, seen);
5502                    }
5503                }
5504            }
5505            Expr::CallExpr { callee, args } => {
5506                Self::collect_symbols_from_expr(callee, exclude, out, seen);
5507                for arg in args {
5508                    Self::collect_symbols_from_expr(arg, exclude, out, seen);
5509                }
5510            }
5511            Expr::InterpolatedString(parts) => {
5512                for part in parts {
5513                    if let crate::ast::stmt::StringPart::Expr { value, .. } = part {
5514                        Self::collect_symbols_from_expr(value, exclude, out, seen);
5515                    }
5516                }
5517            }
5518        }
5519    }
5520
5521    fn collect_symbols_from_block(
5522        stmts: &[Stmt<'a>],
5523        exclude: &std::collections::HashSet<Symbol>,
5524        out: &mut Vec<Symbol>,
5525        seen: &mut std::collections::HashSet<Symbol>,
5526    ) {
5527        use crate::ast::stmt::Pattern;
5528        // `bound` = params + the locals introduced so far in THIS block's scope.
5529        // A `Let` (or `Repeat`/`Inspect` pattern) binding excludes later uses of
5530        // that name from the FREE-variable set — without this a function's own
5531        // local that happens to share a name with a Main top-level variable would
5532        // leak as "free", over-promoting that Main var to a global and blocking
5533        // the JIT from tiering Main's hot loops. Nested blocks clone `bound`, so a
5534        // binding never leaks to a sibling block or past its scope.
5535        let mut bound = exclude.clone();
5536        for stmt in stmts {
5537            match stmt {
5538                Stmt::Let { var, value, .. } => {
5539                    Self::collect_symbols_from_expr(value, &bound, out, seen);
5540                    bound.insert(*var);
5541                }
5542                Stmt::Set { value, .. } => {
5543                    Self::collect_symbols_from_expr(value, &bound, out, seen);
5544                }
5545                Stmt::Call { function, args } => {
5546                    if !bound.contains(function) && seen.insert(*function) {
5547                        out.push(*function);
5548                    }
5549                    for arg in args {
5550                        Self::collect_symbols_from_expr(arg, &bound, out, seen);
5551                    }
5552                }
5553                Stmt::Return { value: Some(e) } => {
5554                    Self::collect_symbols_from_expr(e, &bound, out, seen);
5555                }
5556                Stmt::If { cond, then_block, else_block } => {
5557                    Self::collect_symbols_from_expr(cond, &bound, out, seen);
5558                    Self::collect_symbols_from_block(then_block, &bound, out, seen);
5559                    if let Some(eb) = else_block {
5560                        Self::collect_symbols_from_block(eb, &bound, out, seen);
5561                    }
5562                }
5563                Stmt::While { cond, body, .. } => {
5564                    Self::collect_symbols_from_expr(cond, &bound, out, seen);
5565                    Self::collect_symbols_from_block(body, &bound, out, seen);
5566                }
5567                Stmt::Repeat { pattern, iterable, body } => {
5568                    Self::collect_symbols_from_expr(iterable, &bound, out, seen);
5569                    let mut body_bound = bound.clone();
5570                    match pattern {
5571                        Pattern::Identifier(s) => {
5572                            body_bound.insert(*s);
5573                        }
5574                        Pattern::Tuple(syms) => {
5575                            for s in syms {
5576                                body_bound.insert(*s);
5577                            }
5578                        }
5579                    }
5580                    Self::collect_symbols_from_block(body, &body_bound, out, seen);
5581                }
5582                Stmt::Show { object, .. } | Stmt::Give { object, .. } => {
5583                    Self::collect_symbols_from_expr(object, &bound, out, seen);
5584                }
5585                Stmt::Push { value, collection } | Stmt::Add { value, collection }
5586                | Stmt::Remove { value, collection } => {
5587                    Self::collect_symbols_from_expr(value, &bound, out, seen);
5588                    Self::collect_symbols_from_expr(collection, &bound, out, seen);
5589                }
5590                Stmt::SetIndex { collection, index, value } => {
5591                    Self::collect_symbols_from_expr(collection, &bound, out, seen);
5592                    Self::collect_symbols_from_expr(index, &bound, out, seen);
5593                    Self::collect_symbols_from_expr(value, &bound, out, seen);
5594                }
5595                Stmt::SetField { object, value, .. } => {
5596                    Self::collect_symbols_from_expr(object, &bound, out, seen);
5597                    Self::collect_symbols_from_expr(value, &bound, out, seen);
5598                }
5599                Stmt::RuntimeAssert { condition, .. } => {
5600                    Self::collect_symbols_from_expr(condition, &bound, out, seen);
5601                }
5602                Stmt::Zone { body, .. } => {
5603                    Self::collect_symbols_from_block(body, &bound, out, seen);
5604                }
5605                Stmt::Inspect { target, arms, .. } => {
5606                    Self::collect_symbols_from_expr(target, &bound, out, seen);
5607                    for arm in arms {
5608                        Self::collect_symbols_from_block(arm.body, &bound, out, seen);
5609                    }
5610                }
5611                Stmt::Pop { collection, .. } => {
5612                    Self::collect_symbols_from_expr(collection, &bound, out, seen);
5613                }
5614                _ => {}
5615            }
5616        }
5617    }
5618
5619    /// Execute a closure with pre-evaluated argument values (async).
5620    #[async_recursion(?Send)]
5621    async fn call_closure_value(
5622        &mut self,
5623        closure: &ClosureValue,
5624        mut arg_values: Vec<RuntimeValue>,
5625    ) -> Result<RuntimeValue, String> {
5626        if arg_values.len() != closure.param_names.len() {
5627            return Err(format!(
5628                "Closure expects {} arguments, got {}",
5629                closure.param_names.len(),
5630                arg_values.len()
5631            ));
5632        }
5633
5634        // A SHIPPED generator function carries its body as a sandboxed `GenExpr` (it crossed
5635        // the wire — there is no arena AST to run). Evaluate it directly: total, bounded, no
5636        // frame, no escape. Single-argument arithmetic (the lowerable subset) → an `Int`.
5637        if let Some(expr) = &closure.generated {
5638            let i = match arg_values.first() {
5639                Some(RuntimeValue::Int(n)) => *n,
5640                _ => 0,
5641            };
5642            return Ok(RuntimeValue::Int(crate::concurrency::marshal::gen_eval(expr, i)));
5643        }
5644
5645        // Extract body reference from side-table (breaks borrow on self)
5646        let body_index = closure.body_index;
5647        let is_block = matches!(self.ctx.closure_bodies.get(body_index), Some(ClosureBodyRef::Block(_)));
5648
5649        // A closure body is a fresh frame (lexical barrier): it sees its
5650        // captures, its parameters, and globals — never the caller's locals.
5651        if self.task.call_depth >= crate::semantics::MAX_CALL_DEPTH {
5652            return Err(crate::semantics::CALL_DEPTH_ERR.to_string());
5653        }
5654        self.task.call_depth += 1;
5655        self.task.env.push_frame();
5656
5657        // Bind captured environment
5658        for (sym, val) in &closure.captured_env {
5659            self.task.env.define(*sym, val.deep_clone());
5660        }
5661
5662        // Bind parameters
5663        for (i, param_sym) in closure.param_names.iter().enumerate() {
5664            self.task.env.define(*param_sym, std::mem::replace(&mut arg_values[i], RuntimeValue::Nothing));
5665        }
5666
5667        let result = if is_block {
5668            let block = match &self.ctx.closure_bodies[body_index] {
5669                ClosureBodyRef::Block(b) => *b,
5670                _ => unreachable!(),
5671            };
5672            let mut outcome = Ok(RuntimeValue::Nothing);
5673            for stmt in block.iter() {
5674                match self.execute_stmt(stmt).await {
5675                    Ok(ControlFlow::Return(val)) => {
5676                        outcome = Ok(val);
5677                        break;
5678                    }
5679                    Ok(ControlFlow::Break) => break,
5680                    Ok(ControlFlow::Continue) => {}
5681                    Err(e) => {
5682                        outcome = Err(e);
5683                        break;
5684                    }
5685                }
5686            }
5687            outcome
5688        } else {
5689            let expr = match &self.ctx.closure_bodies[body_index] {
5690                ClosureBodyRef::Expression(e) => *e,
5691                _ => unreachable!(),
5692            };
5693            self.evaluate_expr(expr).await
5694        };
5695
5696        self.task.env.pop_frame();
5697        self.task.call_depth -= 1;
5698        result
5699    }
5700
5701    /// Execute a closure with pre-evaluated argument values (sync).
5702    fn call_closure_value_sync(
5703        &mut self,
5704        closure: &ClosureValue,
5705        mut arg_values: Vec<RuntimeValue>,
5706    ) -> Result<RuntimeValue, String> {
5707        if arg_values.len() != closure.param_names.len() {
5708            return Err(format!(
5709                "Closure expects {} arguments, got {}",
5710                closure.param_names.len(),
5711                arg_values.len()
5712            ));
5713        }
5714
5715        // A SHIPPED generator function evaluates its sandboxed body directly (see async twin).
5716        if let Some(expr) = &closure.generated {
5717            let i = match arg_values.first() {
5718                Some(RuntimeValue::Int(n)) => *n,
5719                _ => 0,
5720            };
5721            return Ok(RuntimeValue::Int(crate::concurrency::marshal::gen_eval(expr, i)));
5722        }
5723
5724        let body_index = closure.body_index;
5725        let is_block = matches!(self.ctx.closure_bodies.get(body_index), Some(ClosureBodyRef::Block(_)));
5726
5727        // A closure body is a fresh frame (lexical barrier); see the async twin.
5728        if self.task.call_depth >= crate::semantics::MAX_CALL_DEPTH {
5729            return Err(crate::semantics::CALL_DEPTH_ERR.to_string());
5730        }
5731        self.task.call_depth += 1;
5732        self.task.env.push_frame();
5733
5734        for (sym, val) in &closure.captured_env {
5735            self.task.env.define(*sym, val.deep_clone());
5736        }
5737
5738        for (i, param_sym) in closure.param_names.iter().enumerate() {
5739            self.task.env.define(*param_sym, std::mem::replace(&mut arg_values[i], RuntimeValue::Nothing));
5740        }
5741
5742        let result = if is_block {
5743            let block = match &self.ctx.closure_bodies[body_index] {
5744                ClosureBodyRef::Block(b) => *b,
5745                _ => unreachable!(),
5746            };
5747            let mut outcome = Ok(RuntimeValue::Nothing);
5748            for stmt in block.iter() {
5749                match self.execute_stmt_sync(stmt) {
5750                    Ok(ControlFlow::Return(val)) => {
5751                        outcome = Ok(val);
5752                        break;
5753                    }
5754                    Ok(ControlFlow::Break) => break,
5755                    Ok(ControlFlow::Continue) => {}
5756                    Err(e) => {
5757                        outcome = Err(e);
5758                        break;
5759                    }
5760                }
5761            }
5762            outcome
5763        } else {
5764            let expr = match &self.ctx.closure_bodies[body_index] {
5765                ClosureBodyRef::Expression(e) => *e,
5766                _ => unreachable!(),
5767            };
5768            self.evaluate_expr_sync(expr)
5769        };
5770
5771        self.task.env.pop_frame();
5772        self.task.call_depth -= 1;
5773        result
5774    }
5775}
5776
5777/// Check whether a program requires async execution.
5778///
5779/// Only 4 statement types need async: ReadFrom (file), WriteFile, Sleep, Mount.
5780/// If none are present, the sync execution path can be used for better performance.
5781fn apply_format_spec(val: &RuntimeValue, spec: &str) -> String {
5782    crate::semantics::format::apply_format_spec(val, spec)
5783}
5784
5785pub fn needs_async(stmts: &[Stmt]) -> bool {
5786    stmts.iter().any(|s| stmt_needs_async(s))
5787}
5788
5789fn stmt_needs_async(stmt: &Stmt) -> bool {
5790    match stmt {
5791        Stmt::ReadFrom { source, .. } => {
5792            matches!(source, ReadSource::File(_))
5793        }
5794        Stmt::WriteFile { .. } | Stmt::Sleep { .. } | Stmt::Mount { .. } => true,
5795        // Networking over the relay is async (dial + subscribe await).
5796        Stmt::Sync { .. } | Stmt::Listen { .. } | Stmt::ConnectTo { .. } => true,
5797        // Peer messaging rides the relay (subscribe/publish/poll) — async only.
5798        Stmt::SendMessage { .. } | Stmt::AwaitMessage { .. } | Stmt::StreamMessage { .. } => true,
5799        Stmt::If { then_block, else_block, .. } => {
5800            needs_async(then_block)
5801                || else_block.as_ref().map_or(false, |b| needs_async(b))
5802        }
5803        Stmt::While { body, .. } | Stmt::Repeat { body, .. } => needs_async(body),
5804        Stmt::FunctionDef { body, .. } => needs_async(body),
5805        Stmt::Zone { body, .. } => needs_async(body),
5806        Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => needs_async(tasks),
5807        Stmt::Inspect { arms, .. } => arms.iter().any(|arm| needs_async(arm.body)),
5808        _ => false,
5809    }
5810}
5811
5812/// Phase 102: Count the number of Pi (function) arguments in a kernel Term.
5813///
5814/// Used to determine constructor arity for inductive types.
5815fn count_pi_args(term: &crate::kernel::Term) -> usize {
5816    use crate::kernel::Term;
5817    match term {
5818        Term::Pi { body_type, .. } => 1 + count_pi_args(body_type),
5819        _ => 0,
5820    }
5821}
5822
5823/// Result from program interpretation.
5824///
5825/// Contains both the output produced by `show()` calls and any error
5826/// that occurred during execution. Used by the UI bridge to display
5827/// program output to users.
5828#[derive(Debug, Clone)]
5829pub struct InterpreterResult {
5830    /// Output lines from `show()` calls during execution.
5831    pub lines: Vec<String>,
5832    /// Error message if execution failed, or `None` on success.
5833    pub error: Option<String>,
5834}
5835
5836#[cfg(test)]
5837mod ints_i32_repr_tests {
5838    use super::*;
5839
5840    fn i32_buf(vals: &[i64]) -> ListRepr {
5841        let mut r = ListRepr::IntsI32(Vec::new());
5842        for &v in vals {
5843            r.push(RuntimeValue::Int(v));
5844        }
5845        r
5846    }
5847
5848    #[test]
5849    fn push_and_get_sign_extend() {
5850        let r = i32_buf(&[-1, 0, 7, i32::MIN as i64, i32::MAX as i64]);
5851        assert!(matches!(r, ListRepr::IntsI32(_)), "stays half-width when every value fits i32");
5852        assert_eq!(r.get(0), Some(RuntimeValue::Int(-1)), "negative sign-extends losslessly");
5853        assert_eq!(r.get(3), Some(RuntimeValue::Int(i32::MIN as i64)));
5854        assert_eq!(r.get(4), Some(RuntimeValue::Int(i32::MAX as i64)));
5855        assert_eq!(r.len(), 5);
5856    }
5857
5858    #[test]
5859    fn push_out_of_range_widens_and_preserves_values() {
5860        // A value just past i32::MAX forces the whole buffer to full width;
5861        // every earlier element survives, bit-identical to a never-narrowed run.
5862        let mut r = i32_buf(&[1, -2, 100]);
5863        r.push(RuntimeValue::Int(i32::MAX as i64 + 1));
5864        assert!(matches!(r, ListRepr::Ints(_)), "an out-of-range push widens to full-width Ints");
5865        assert_eq!(r.get(0), Some(RuntimeValue::Int(1)));
5866        assert_eq!(r.get(1), Some(RuntimeValue::Int(-2)));
5867        assert_eq!(r.get(2), Some(RuntimeValue::Int(100)));
5868        assert_eq!(r.get(3), Some(RuntimeValue::Int(i32::MAX as i64 + 1)));
5869    }
5870
5871    #[test]
5872    fn set_out_of_range_widens() {
5873        let mut r = i32_buf(&[5, 5, 5]);
5874        r.set(1, RuntimeValue::Int(i64::MIN));
5875        assert!(matches!(r, ListRepr::Ints(_)), "an out-of-range in-place store widens");
5876        assert_eq!(r.get(0), Some(RuntimeValue::Int(5)));
5877        assert_eq!(r.get(1), Some(RuntimeValue::Int(i64::MIN)));
5878        assert_eq!(r.get(2), Some(RuntimeValue::Int(5)));
5879    }
5880
5881    #[test]
5882    fn set_in_range_truncates_losslessly() {
5883        let mut r = i32_buf(&[0, 0, 0]);
5884        r.set(2, RuntimeValue::Int(-12345));
5885        assert!(matches!(r, ListRepr::IntsI32(_)), "in-range store stays half-width");
5886        assert_eq!(r.get(2), Some(RuntimeValue::Int(-12345)));
5887    }
5888
5889    #[test]
5890    fn non_int_push_promotes_to_boxed() {
5891        // Soundness net: a narrowed buffer that somehow receives a non-Int value
5892        // boxes rather than dropping the type — never silently wrong.
5893        let mut r = i32_buf(&[1, 2]);
5894        r.push(RuntimeValue::Float(3.5));
5895        assert!(matches!(r, ListRepr::Boxed(_)));
5896        assert_eq!(r.get(2), Some(RuntimeValue::Float(3.5)));
5897        assert_eq!(r.get(0), Some(RuntimeValue::Int(1)));
5898    }
5899
5900    #[test]
5901    fn clone_round_trips_to_values() {
5902        let r = i32_buf(&[-7, 42, i32::MIN as i64]);
5903        let snap = r.clone();
5904        assert_eq!(snap.to_values(), r.to_values());
5905        assert_eq!(
5906            r.to_values(),
5907            vec![
5908                RuntimeValue::Int(-7),
5909                RuntimeValue::Int(42),
5910                RuntimeValue::Int(i32::MIN as i64)
5911            ]
5912        );
5913    }
5914
5915    #[test]
5916    fn pop_truncate_position_match_full_width() {
5917        let mut r = i32_buf(&[10, 20, 30]);
5918        assert_eq!(r.position(&RuntimeValue::Int(20)), Some(1));
5919        assert_eq!(r.position(&RuntimeValue::Int(99)), None);
5920        assert_eq!(r.pop(), Some(RuntimeValue::Int(30)));
5921        r.truncate(1);
5922        assert_eq!(r.len(), 1);
5923        assert_eq!(r.get(0), Some(RuntimeValue::Int(10)));
5924    }
5925}
5926
5927#[cfg(test)]
5928mod structs_repr_tests {
5929    use super::*;
5930    use std::collections::HashMap;
5931
5932    fn point(x: i64, y: i64) -> RuntimeValue {
5933        let mut f = HashMap::new();
5934        f.insert("x".to_string(), RuntimeValue::Int(x));
5935        f.insert("y".to_string(), RuntimeValue::Int(y));
5936        RuntimeValue::Struct(Box::new(StructValue { type_name: "Point".to_string(), fields: f }))
5937    }
5938    fn int_field(v: &RuntimeValue, name: &str) -> i64 {
5939        match v {
5940            RuntimeValue::Struct(sv) => match sv.fields.get(name) {
5941                Some(RuntimeValue::Int(n)) => *n,
5942                other => panic!("field {name} not an int: {other:?}"),
5943            },
5944            other => panic!("not a struct: {other:?}"),
5945        }
5946    }
5947
5948    #[test]
5949    fn from_values_homogeneous_structs_is_columnar() {
5950        let r = ListRepr::from_values(vec![point(0, 0), point(1, 2), point(2, 4)]);
5951        assert!(matches!(r, ListRepr::Structs { .. }), "a homogeneous struct list de-boxes to columns");
5952        assert_eq!(r.len(), 3);
5953        assert!(!r.is_empty());
5954    }
5955
5956    #[test]
5957    fn structs_get_reconstructs_exact() {
5958        let r = ListRepr::from_values(vec![point(0, 0), point(1, 2), point(2, 4)]);
5959        let s = r.get(1).unwrap();
5960        assert_eq!(int_field(&s, "x"), 1);
5961        assert_eq!(int_field(&s, "y"), 2);
5962        assert!(r.get(3).is_none(), "out-of-range index is None");
5963    }
5964
5965    #[test]
5966    fn structs_to_values_reconstructs_all_rows() {
5967        let r = ListRepr::from_values(vec![point(5, 6), point(7, 8)]);
5968        let vs = r.to_values();
5969        assert_eq!(vs.len(), 2);
5970        assert_eq!(int_field(&vs[0], "x"), 5);
5971        assert_eq!(int_field(&vs[1], "y"), 8);
5972    }
5973
5974    #[test]
5975    fn structs_truncate_is_columnwise() {
5976        let mut r = ListRepr::from_values(vec![point(0, 0), point(1, 1), point(2, 2)]);
5977        r.truncate(2);
5978        assert!(matches!(r, ListRepr::Structs { .. }), "truncate keeps it columnar");
5979        assert_eq!(r.len(), 2);
5980        assert_eq!(int_field(&r.get(1).unwrap(), "x"), 1);
5981    }
5982
5983    #[test]
5984    fn columnar_field_read_is_direct() {
5985        let r = ListRepr::from_values(vec![point(10, 20), point(30, 40)]);
5986        // get_field reads one column directly — no StructValue reconstruction.
5987        assert_eq!(r.get_field(0, "x"), Some(RuntimeValue::Int(10)));
5988        assert_eq!(r.get_field(1, "y"), Some(RuntimeValue::Int(40)));
5989        assert_eq!(r.get_field(0, "z"), None, "missing field");
5990        assert!(r.get_field(5, "x").is_none(), "out of range");
5991        // the column accessor exposes the raw packed column for array-speed scans.
5992        match r.column("x") {
5993            Some(ListRepr::Ints(v)) => assert_eq!(v, &vec![10, 30]),
5994            other => panic!("expected an Ints column, got {other:?}"),
5995        }
5996        // a boxed list has no columns.
5997        let boxed = ListRepr::Boxed(vec![point(1, 2)]);
5998        assert!(boxed.get_field(0, "x").is_none());
5999        assert!(boxed.column("x").is_none());
6000    }
6001
6002    #[test]
6003    fn structs_mutation_decolumnarizes_and_stays_correct() {
6004        // A set to a non-struct value de-columnarizes (make_boxed) but preserves
6005        // every prior row exactly — the soundness invariant.
6006        let mut r = ListRepr::from_values(vec![point(0, 0), point(1, 1), point(2, 2)]);
6007        r.set(1, RuntimeValue::Int(99));
6008        assert!(matches!(r, ListRepr::Boxed(_)), "a mutating set de-columnarizes");
6009        assert_eq!(int_field(&r.get(0).unwrap(), "x"), 0);
6010        assert_eq!(r.get(1), Some(RuntimeValue::Int(99)));
6011        assert_eq!(int_field(&r.get(2).unwrap(), "y"), 2);
6012    }
6013
6014    #[test]
6015    fn structs_push_stays_correct() {
6016        let mut r = ListRepr::from_values(vec![point(0, 0)]);
6017        r.push(point(1, 1));
6018        assert_eq!(r.len(), 2);
6019        assert_eq!(int_field(&r.get(0).unwrap(), "x"), 0);
6020        assert_eq!(int_field(&r.get(1).unwrap(), "x"), 1);
6021    }
6022
6023    #[test]
6024    fn heterogeneous_structs_stay_boxed() {
6025        // Ragged field set ⇒ boxed.
6026        let mut only_x = HashMap::new();
6027        only_x.insert("x".to_string(), RuntimeValue::Int(9));
6028        let odd = RuntimeValue::Struct(Box::new(StructValue { type_name: "Point".to_string(), fields: only_x }));
6029        let r = ListRepr::from_values(vec![point(0, 0), odd]);
6030        assert!(matches!(r, ListRepr::Boxed(_)), "ragged field sets stay boxed");
6031
6032        // Mixed type names ⇒ boxed.
6033        let mut cf = HashMap::new();
6034        cf.insert("x".to_string(), RuntimeValue::Int(1));
6035        cf.insert("y".to_string(), RuntimeValue::Int(2));
6036        let q = RuntimeValue::Struct(Box::new(StructValue { type_name: "Other".to_string(), fields: cf }));
6037        let r2 = ListRepr::from_values(vec![point(0, 0), q]);
6038        assert!(matches!(r2, ListRepr::Boxed(_)), "mixed type names stay boxed");
6039    }
6040
6041    #[test]
6042    fn columnar_field_scan_is_faster_than_boxed_and_iteration_is_not_slower() {
6043        // The in-memory win, measured on the shared `ListRepr` primitive (both the
6044        // tree-walker and the VM read lists through it). Reported with --nocapture;
6045        // the asserts are noise-robust (huge margins / not-slower).
6046        use std::time::Instant;
6047        const N: usize = 5000;
6048        const ITERS: u32 = 300;
6049
6050        let rows: Vec<RuntimeValue> = (0..N as i64).map(|i| point(i, i * 2)).collect();
6051        let columnar = ListRepr::from_values(rows.clone());
6052        assert!(matches!(columnar, ListRepr::Structs { .. }), "the columnar baseline must be Structs");
6053        let boxed = ListRepr::Boxed(rows);
6054
6055        // (a) FIELD SCAN — sum the "x" field across the list. Columnar reads the raw
6056        //     Vec<i64> column (array speed); boxed reconstructs/clones each struct and
6057        //     hashmap-looks-up "x". This is the "arrays not heap" win.
6058        let scan_columnar = || -> i64 {
6059            match columnar.column("x") {
6060                Some(ListRepr::Ints(v)) => v.iter().copied().sum(),
6061                _ => unreachable!(),
6062            }
6063        };
6064        let scan_boxed = || -> i64 {
6065            let mut s = 0i64;
6066            for i in 0..boxed.len() {
6067                if let Some(RuntimeValue::Struct(sv)) = boxed.get(i) {
6068                    if let Some(RuntimeValue::Int(x)) = sv.fields.get("x") {
6069                        s += *x;
6070                    }
6071                }
6072            }
6073            s
6074        };
6075        assert_eq!(scan_columnar(), scan_boxed(), "the two scans must agree");
6076
6077        let t = Instant::now();
6078        for _ in 0..ITERS {
6079            std::hint::black_box(scan_columnar());
6080        }
6081        let col_ns = t.elapsed().as_nanos().max(1);
6082        let t = Instant::now();
6083        for _ in 0..ITERS {
6084            std::hint::black_box(scan_boxed());
6085        }
6086        let box_ns = t.elapsed().as_nanos().max(1);
6087        println!(
6088            "\n[E3] field scan (sum x over {N}, ×{ITERS}): columnar {col_ns} ns vs boxed {box_ns} ns  ({:.1}× faster)",
6089            box_ns as f64 / col_ns as f64
6090        );
6091        assert!(col_ns * 2 <= box_ns, "columnar field scan should be ≥2× faster: columnar {col_ns} vs boxed {box_ns}");
6092
6093        // (b) FULL-ROW iteration — reconstruct every struct. Roughly a wash (both
6094        //     reprs box a StructValue per element), reported for honesty; asserted only
6095        //     "not catastrophically slower".
6096        let iter_repr = |r: &ListRepr| {
6097            let mut acc = 0i64;
6098            for i in 0..r.len() {
6099                if let Some(RuntimeValue::Struct(sv)) = r.get(i) {
6100                    if let (Some(RuntimeValue::Int(x)), Some(RuntimeValue::Int(y))) =
6101                        (sv.fields.get("x"), sv.fields.get("y"))
6102                    {
6103                        acc += x + y;
6104                    }
6105                }
6106            }
6107            acc
6108        };
6109        assert_eq!(iter_repr(&columnar), iter_repr(&boxed), "full-row iteration must agree");
6110        let t = Instant::now();
6111        for _ in 0..ITERS {
6112            std::hint::black_box(iter_repr(&columnar));
6113        }
6114        let col_it = t.elapsed().as_nanos().max(1);
6115        let t = Instant::now();
6116        for _ in 0..ITERS {
6117            std::hint::black_box(iter_repr(&boxed));
6118        }
6119        let box_it = t.elapsed().as_nanos().max(1);
6120        println!(
6121            "[E3] full-row iter (×{ITERS}): columnar {col_it} ns vs boxed {box_it} ns  ({:.2}× of boxed)",
6122            col_it as f64 / box_it as f64
6123        );
6124        assert!(col_it <= box_it * 3, "full-row iteration must not be catastrophically slower: {col_it} vs {box_it}");
6125    }
6126
6127    #[test]
6128    fn zero_field_structs_stay_boxed_and_keep_count() {
6129        // A columnar store has no column to carry the row count when the struct has
6130        // no fields, so such a list MUST stay boxed (and report the right length).
6131        let unit = || RuntimeValue::Struct(Box::new(StructValue { type_name: "Unit".to_string(), fields: HashMap::new() }));
6132        let r = ListRepr::from_values(vec![unit(), unit(), unit()]);
6133        assert!(matches!(r, ListRepr::Boxed(_)), "zero-field struct list stays boxed");
6134        assert_eq!(r.len(), 3, "row count is preserved");
6135    }
6136}
6137
6138#[cfg(test)]
6139mod enums_repr_tests {
6140    use super::*;
6141
6142    fn nullary(ty: &str, ctor: &str) -> RuntimeValue {
6143        RuntimeValue::Inductive(Box::new(InductiveValue { inductive_type: ty.into(), constructor: ctor.into(), args: vec![] }))
6144    }
6145    fn with_args(ty: &str, ctor: &str, args: Vec<RuntimeValue>) -> RuntimeValue {
6146        RuntimeValue::Inductive(Box::new(InductiveValue { inductive_type: ty.into(), constructor: ctor.into(), args }))
6147    }
6148    fn ctor_of(v: &RuntimeValue) -> String {
6149        match v {
6150            RuntimeValue::Inductive(i) => i.constructor.clone(),
6151            other => panic!("not an inductive: {other:?}"),
6152        }
6153    }
6154    fn int_arg(v: &RuntimeValue, j: usize) -> i64 {
6155        match v {
6156            RuntimeValue::Inductive(i) => match &i.args[j] {
6157                RuntimeValue::Int(n) => *n,
6158                other => panic!("arg {j} not an int: {other:?}"),
6159            },
6160            other => panic!("not an inductive: {other:?}"),
6161        }
6162    }
6163
6164    #[test]
6165    fn from_values_nullary_enums_is_columnar() {
6166        let r = ListRepr::from_values(vec![nullary("Color", "Red"), nullary("Color", "Green"), nullary("Color", "Red")]);
6167        assert!(matches!(r, ListRepr::Inductives { .. }), "a nullary enum list de-boxes to columns");
6168        assert_eq!(r.len(), 3);
6169        assert_eq!(ctor_of(&r.get(0).unwrap()), "Red");
6170        assert_eq!(ctor_of(&r.get(1).unwrap()), "Green");
6171        assert_eq!(ctor_of(&r.get(2).unwrap()), "Red");
6172        assert!(r.get(3).is_none());
6173    }
6174
6175    #[test]
6176    fn from_values_uniform_arg_enums_is_columnar() {
6177        let r = ListRepr::from_values(vec![
6178            with_args("Boxed", "B", vec![RuntimeValue::Int(1)]),
6179            with_args("Boxed", "B", vec![RuntimeValue::Int(2)]),
6180        ]);
6181        assert!(matches!(r, ListRepr::Inductives { .. }), "a uniform-arg enum list packs columnar");
6182        assert_eq!(int_arg(&r.get(0).unwrap(), 0), 1);
6183        assert_eq!(int_arg(&r.get(1).unwrap(), 0), 2);
6184    }
6185
6186    #[test]
6187    fn from_values_mixed_arity_enums_is_columnar() {
6188        // Option-like: Some(1), None, Some(2), None, Some(3) — a tagged union, packed
6189        // as dense per-constructor arg columns.
6190        let rows = vec![
6191            with_args("Option", "Some", vec![RuntimeValue::Int(1)]),
6192            nullary("Option", "None"),
6193            with_args("Option", "Some", vec![RuntimeValue::Int(2)]),
6194            nullary("Option", "None"),
6195            with_args("Option", "Some", vec![RuntimeValue::Int(3)]),
6196        ];
6197        let r = ListRepr::from_values(rows);
6198        assert!(matches!(r, ListRepr::Inductives { .. }), "a mixed-arity enum list packs columnar");
6199        assert_eq!(r.len(), 5);
6200        assert_eq!(ctor_of(&r.get(0).unwrap()), "Some");
6201        assert_eq!(int_arg(&r.get(0).unwrap(), 0), 1);
6202        assert_eq!(ctor_of(&r.get(1).unwrap()), "None");
6203        assert_eq!(ctor_of(&r.get(2).unwrap()), "Some");
6204        assert_eq!(int_arg(&r.get(2).unwrap(), 0), 2);
6205        assert_eq!(ctor_of(&r.get(3).unwrap()), "None");
6206        assert_eq!(ctor_of(&r.get(4).unwrap()), "Some");
6207        assert_eq!(int_arg(&r.get(4).unwrap(), 0), 3);
6208    }
6209
6210    #[test]
6211    fn enums_to_values_reconstructs_all_rows() {
6212        let rows = vec![with_args("Option", "Some", vec![RuntimeValue::Int(7)]), nullary("Option", "None")];
6213        let vs = ListRepr::from_values(rows).to_values();
6214        assert_eq!(vs.len(), 2);
6215        assert_eq!(ctor_of(&vs[0]), "Some");
6216        assert_eq!(int_arg(&vs[0], 0), 7);
6217        assert_eq!(ctor_of(&vs[1]), "None");
6218    }
6219
6220    #[test]
6221    fn enums_mutation_decolumnarizes_and_stays_correct() {
6222        let mut r = ListRepr::from_values(vec![nullary("Color", "Red"), nullary("Color", "Green")]);
6223        r.set(1, RuntimeValue::Int(99));
6224        assert!(matches!(r, ListRepr::Boxed(_)), "a mutating set de-columnarizes");
6225        assert_eq!(ctor_of(&r.get(0).unwrap()), "Red");
6226        assert_eq!(r.get(1), Some(RuntimeValue::Int(99)));
6227    }
6228
6229    #[test]
6230    fn heterogeneous_enums_stay_boxed() {
6231        let r = ListRepr::from_values(vec![nullary("Color", "Red"), nullary("Suit", "Spade")]);
6232        assert!(matches!(r, ListRepr::Boxed(_)), "mixed inductive types stay boxed");
6233    }
6234}
6235
6236#[cfg(test)]
6237mod float_comparison_tests {
6238    use super::*;
6239
6240    fn expect_bool(r: Result<RuntimeValue, String>, want: bool, label: &str) {
6241        match r {
6242            Ok(RuntimeValue::Bool(b)) => assert_eq!(b, want, "{label}"),
6243            other => panic!("{label}: expected Bool, got {:?}", other.map(|v| v.type_name().to_string())),
6244        }
6245    }
6246
6247    #[test]
6248    fn float_relational_uses_ieee_semantics() {
6249        use RuntimeValue::{Float, Int};
6250        let interner = Interner::new();
6251        let interp = Interpreter::new(&interner);
6252        let nan = f64::NAN;
6253
6254        // -0.0 and +0.0 are equal under IEEE 754.
6255        expect_bool(interp.apply_binary_op(BinaryOpKind::Lt, Float(-0.0), Float(0.0)), false, "-0.0 < 0.0");
6256        expect_bool(interp.apply_binary_op(BinaryOpKind::Gt, Float(0.0), Float(-0.0)), false, "0.0 > -0.0");
6257        expect_bool(interp.apply_binary_op(BinaryOpKind::LtEq, Float(-0.0), Float(0.0)), true, "-0.0 <= 0.0");
6258        expect_bool(interp.apply_binary_op(BinaryOpKind::GtEq, Float(-0.0), Float(0.0)), true, "-0.0 >= 0.0");
6259
6260        // NaN is unordered: every relational comparison is false.
6261        expect_bool(interp.apply_binary_op(BinaryOpKind::Lt, Float(nan), Float(1.0)), false, "NaN < 1");
6262        expect_bool(interp.apply_binary_op(BinaryOpKind::Gt, Float(nan), Float(1.0)), false, "NaN > 1");
6263        expect_bool(interp.apply_binary_op(BinaryOpKind::LtEq, Float(nan), Float(nan)), false, "NaN <= NaN");
6264        expect_bool(interp.apply_binary_op(BinaryOpKind::GtEq, Float(1.0), Float(nan)), false, "1 >= NaN");
6265
6266        // Ordinary comparisons still work, including mixed Int/Float and pure Int.
6267        expect_bool(interp.apply_binary_op(BinaryOpKind::Lt, Float(1.5), Float(2.5)), true, "1.5 < 2.5");
6268        expect_bool(interp.apply_binary_op(BinaryOpKind::Lt, Int(2), Float(2.5)), true, "2 < 2.5");
6269        expect_bool(interp.apply_binary_op(BinaryOpKind::GtEq, Float(2.5), Int(2)), true, "2.5 >= 2");
6270        expect_bool(interp.apply_binary_op(BinaryOpKind::Lt, Int(3), Int(5)), true, "3 < 5");
6271        expect_bool(interp.apply_binary_op(BinaryOpKind::GtEq, Int(5), Int(5)), true, "5 >= 5");
6272    }
6273}