Skip to main content

bock_interp/
interp.rs

1//! Tree-walking interpreter for Bock AIR expressions.
2
3use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::sync::{Arc, Mutex};
5
6use async_recursion::async_recursion;
7use futures::future::BoxFuture;
8
9use bock_air::{
10    AIRNode, AirArg, AirInterpolationPart, AirRecordField, EnumVariantPayload, NodeKind,
11    ResultVariant,
12};
13use bock_ast::{AssignOp, BinOp, Literal, TypePath, UnaryOp};
14
15use crate::builtins::{BuiltinRegistry, CallbackInvoker, TypeTag};
16use crate::env::{EffectStack, Environment};
17use crate::error::RuntimeError;
18use crate::value::{BockString, EnumValue, FnValue, IteratorNext, OrdF64, RecordValue, Value};
19
20// ─── Closure ──────────────────────────────────────────────────────────────────
21
22/// A reference-counted native constructor function.
23type NativeFn = std::sync::Arc<dyn Fn(&[Value]) -> Value + Send + Sync>;
24
25/// The body of a closure — either an AIR node, a composition, or a native Rust function.
26#[derive(Clone)]
27enum ClosureBody {
28    /// A regular lambda / function body.
29    Air(Box<AIRNode>),
30    /// A composed function: apply `inner` first, pass result to `outer`.
31    Composed { inner: u64, outer: u64 },
32    /// A native constructor function implemented in Rust.
33    Native(NativeFn),
34}
35
36impl std::fmt::Debug for ClosureBody {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            ClosureBody::Air(node) => f.debug_tuple("Air").field(node).finish(),
40            ClosureBody::Composed { inner, outer } => f
41                .debug_struct("Composed")
42                .field("inner", inner)
43                .field("outer", outer)
44                .finish(),
45            ClosureBody::Native(_) => f.debug_tuple("Native").field(&"<fn>").finish(),
46        }
47    }
48}
49
50/// A closure: parameter names, body, and the environment captured at definition.
51#[derive(Debug, Clone)]
52struct Closure {
53    params: Vec<String>,
54    body: ClosureBody,
55    captured: Environment,
56    /// True for named top-level functions. When called, these use the current
57    /// interpreter environment (which contains all registered globals) instead
58    /// of the captured snapshot. This enables recursion, mutual recursion, and
59    /// forward references between top-level functions.
60    is_toplevel: bool,
61    /// True for `async fn` declarations. When true, calling the closure
62    /// spawns the body as a tokio task and returns `Value::Future` immediately.
63    is_async: bool,
64}
65
66// ─── Interpreter ──────────────────────────────────────────────────────────────
67
68/// Tree-walking interpreter for Bock AIR.
69///
70/// Evaluates expressions against a typed AIR tree. The `Environment` manages
71/// lexical variable bindings; the `EffectStack` will be used by P5.5 for
72/// algebraic effect dispatch.
73#[derive(Clone)]
74pub struct Interpreter {
75    /// Current variable bindings (nested scopes).
76    pub env: Environment,
77    /// Algebraic effect handler stack (populated by P5.5).
78    pub effect_handlers: EffectStack,
79    /// Maps `FnValue::id` to the corresponding closure implementation.
80    fn_registry: HashMap<u64, Closure>,
81    /// Built-in method and global function dispatch table.
82    pub builtins: BuiltinRegistry,
83    /// User-defined impl methods: type_name → method_name → (param_names, body).
84    method_table: HashMap<String, HashMap<String, (Vec<String>, AIRNode)>>,
85    /// Maps effect operation names to their parent effect name.
86    /// Used to dispatch `log(msg)` → look up handler for `Logger` → call it.
87    effect_operations: HashMap<String, String>,
88}
89
90impl CallbackInvoker for Interpreter {
91    fn invoke<'a>(
92        &'a mut self,
93        callable: &'a Value,
94        args: &'a [Value],
95    ) -> BoxFuture<'a, Result<Value, RuntimeError>> {
96        Box::pin(async move { self.invoke_callback(callable, args).await })
97    }
98}
99
100impl Default for Interpreter {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl Interpreter {
107    /// Create a new interpreter with an empty global environment
108    /// and default built-in functions registered.
109    #[must_use]
110    pub fn new() -> Self {
111        let mut builtins = BuiltinRegistry::new();
112        builtins.register_defaults();
113        let mut interp = Self {
114            env: Environment::new(),
115            effect_handlers: EffectStack::new(),
116            fn_registry: HashMap::new(),
117            builtins,
118            method_table: HashMap::new(),
119            effect_operations: HashMap::new(),
120        };
121        interp.register_prelude_constructors();
122        interp
123    }
124
125    /// Register the built-in `Ok`, `Err`, `Some`, and `None` constructors
126    /// in the global environment so they are available at runtime.
127    fn register_prelude_constructors(&mut self) {
128        // None is a plain value, not a function.
129        self.env.define("None", Value::Optional(None));
130
131        // Some(x) → Value::Optional(Some(x))
132        self.register_native_constructor("Some", 1, |args| {
133            Value::Optional(Some(Box::new(args[0].clone())))
134        });
135
136        // Ok(x) → Value::Result(Ok(x))
137        self.register_native_constructor("Ok", 1, |args| {
138            Value::Result(Ok(Box::new(args[0].clone())))
139        });
140
141        // Err(x) → Value::Result(Err(x))
142        self.register_native_constructor("Err", 1, |args| {
143            Value::Result(Err(Box::new(args[0].clone())))
144        });
145    }
146
147    /// Register a native constructor function (not backed by an AIR body).
148    ///
149    /// The `build` closure receives the validated arguments and returns the
150    /// constructed `Value`.
151    fn register_native_constructor(
152        &mut self,
153        name: &str,
154        arity: usize,
155        build: impl Fn(&[Value]) -> Value + Send + Sync + 'static,
156    ) {
157        let fn_val = FnValue::new_named(name);
158        let id = fn_val.id;
159        let params: Vec<String> = (0..arity).map(|i| format!("__arg{i}")).collect();
160        self.fn_registry.insert(
161            id,
162            Closure {
163                params,
164                body: ClosureBody::Native(std::sync::Arc::new(build)),
165                captured: Environment::new(),
166                is_toplevel: true,
167                is_async: false,
168            },
169        );
170        self.env.define(name, Value::Function(fn_val));
171    }
172
173    /// Register all variants of an enum declaration in the environment.
174    ///
175    /// - Unit variants become `Value::Enum` values.
176    /// - Tuple variants become constructor functions wrapping args in `Value::Enum`.
177    /// - Record (struct) variants become constructor functions producing `Value::Record`.
178    pub fn register_enum(&mut self, enum_name: &str, variants: &[AIRNode]) {
179        let type_name = enum_name.to_string();
180        for variant in variants {
181            if let NodeKind::EnumVariant { name, payload } = &variant.kind {
182                let variant_name = name.name.clone();
183                match payload {
184                    EnumVariantPayload::Unit => {
185                        self.env.define(
186                            variant_name.clone(),
187                            Value::Enum(EnumValue {
188                                type_name: type_name.clone(),
189                                variant: variant_name,
190                                payload: None,
191                            }),
192                        );
193                    }
194                    EnumVariantPayload::Tuple(fields) => {
195                        let arity = fields.len();
196                        let tn = type_name.clone();
197                        let vn = variant_name.clone();
198                        if arity == 1 {
199                            self.register_native_constructor(&variant_name, arity, move |args| {
200                                Value::Enum(EnumValue {
201                                    type_name: tn.clone(),
202                                    variant: vn.clone(),
203                                    payload: Some(Box::new(args[0].clone())),
204                                })
205                            });
206                        } else {
207                            self.register_native_constructor(&variant_name, arity, move |args| {
208                                Value::Enum(EnumValue {
209                                    type_name: tn.clone(),
210                                    variant: vn.clone(),
211                                    payload: Some(Box::new(Value::Tuple(args.to_vec()))),
212                                })
213                            });
214                        }
215                    }
216                    EnumVariantPayload::Struct(fields) => {
217                        let arity = fields.len();
218                        let vn = variant_name.clone();
219                        let field_names: Vec<String> =
220                            fields.iter().map(|f| f.name.name.clone()).collect();
221                        self.register_native_constructor(&variant_name, arity, move |args| {
222                            let mut field_map = std::collections::BTreeMap::new();
223                            for (fname, val) in field_names.iter().zip(args.iter()) {
224                                field_map.insert(fname.clone(), val.clone());
225                            }
226                            Value::Record(RecordValue {
227                                type_name: vn.clone(),
228                                fields: field_map,
229                            })
230                        });
231                    }
232                }
233            }
234        }
235    }
236
237    /// Register a named function in the global environment.
238    ///
239    /// The function is stored in the registry and bound by name so that
240    /// `Identifier` nodes referencing it will resolve to `Value::Function`.
241    pub fn register_fn(&mut self, name: &str, params: Vec<String>, body: AIRNode) {
242        self.register_fn_with_async(name, params, body, false);
243    }
244
245    /// Register a named function whose body may be `async`.
246    ///
247    /// When `is_async` is true, calling the function spawns a tokio task and
248    /// returns a `Value::Future`; when false, it runs inline like a regular
249    /// function call.
250    pub fn register_fn_with_async(
251        &mut self,
252        name: &str,
253        params: Vec<String>,
254        body: AIRNode,
255        is_async: bool,
256    ) {
257        let fn_val = FnValue::new_named(name);
258        let id = fn_val.id;
259        self.env.define(name, Value::Function(fn_val));
260        self.fn_registry.insert(
261            id,
262            Closure {
263                params,
264                body: ClosureBody::Air(Box::new(body)),
265                captured: Environment::new(),
266                is_toplevel: true,
267                is_async,
268            },
269        );
270    }
271
272    /// Register methods from an `impl` block in the method table.
273    ///
274    /// Extracts the target type name and each method's parameter names + body,
275    /// storing them in `method_table[type_name][method_name]`.
276    pub fn register_impl(&mut self, target: &AIRNode, methods: &[AIRNode]) {
277        // Extract the type name from the target node (TypeNamed path).
278        let type_name = match &target.kind {
279            NodeKind::TypeNamed { path, .. } => path
280                .segments
281                .last()
282                .map(|s| s.name.clone())
283                .unwrap_or_default(),
284            _ => return,
285        };
286
287        let type_methods = self.method_table.entry(type_name).or_default();
288        for method in methods {
289            if let NodeKind::FnDecl {
290                name, params, body, ..
291            } = &method.kind
292            {
293                let param_names: Vec<String> = params
294                    .iter()
295                    .filter_map(|p| {
296                        if let NodeKind::Param { pattern, .. } = &p.kind {
297                            if let NodeKind::BindPat { name, .. } = &pattern.kind {
298                                return Some(name.name.clone());
299                            }
300                        }
301                        None
302                    })
303                    .collect();
304                type_methods.insert(name.name.clone(), (param_names, *body.clone()));
305            }
306        }
307    }
308
309    /// Register an effect declaration's operations so they can be dispatched
310    /// at runtime through the effect handler stack.
311    ///
312    /// For each operation in the effect, records a mapping from the operation
313    /// name to the effect name. When a call like `log(msg)` is evaluated,
314    /// the interpreter checks this map, finds the effect (`Logger`), resolves
315    /// the handler, and dispatches the call.
316    pub fn register_effect(&mut self, effect_name: &str, operations: &[AIRNode]) {
317        // Also define the effect name in the environment (type-level marker).
318        self.env.define(effect_name, Value::Void);
319
320        for op in operations {
321            if let NodeKind::FnDecl { name, .. } = &op.kind {
322                self.effect_operations
323                    .insert(name.name.clone(), effect_name.to_string());
324            }
325        }
326    }
327
328    /// Invoke a callable (function value) using the interpreter's closure machinery.
329    ///
330    /// This is the public entry point for callback invocation from builtins.
331    #[async_recursion]
332    pub async fn invoke_callback(
333        &mut self,
334        callable: &Value,
335        args: &[Value],
336    ) -> Result<Value, RuntimeError> {
337        let fn_id = match callable {
338            Value::Function(fv) => fv.id,
339            other => {
340                return Err(RuntimeError::NotCallable {
341                    value: other.to_string(),
342                })
343            }
344        };
345        let closure =
346            self.fn_registry
347                .get(&fn_id)
348                .cloned()
349                .ok_or_else(|| RuntimeError::NotCallable {
350                    value: format!("unregistered fn #{fn_id}"),
351                })?;
352        self.call_closure(&closure, args.to_vec()).await
353    }
354
355    // ── Main evaluation entry point ────────────────────────────────────────
356
357    /// Evaluate a single AIR expression node and return its runtime value.
358    #[async_recursion]
359    pub async fn eval_expr(&mut self, node: &AIRNode) -> Result<Value, RuntimeError> {
360        match &node.kind {
361            NodeKind::Literal { lit } => self.eval_literal(lit),
362
363            NodeKind::Identifier { name } => {
364                self.env
365                    .get(&name.name)
366                    .cloned()
367                    .ok_or_else(|| RuntimeError::UndefinedVariable {
368                        name: name.name.clone(),
369                    })
370            }
371
372            NodeKind::BinaryOp { op, left, right } => self.eval_binary_op(*op, left, right).await,
373
374            NodeKind::UnaryOp { op, operand } => self.eval_unary_op(*op, operand).await,
375
376            NodeKind::Assign { op, target, value } => self.eval_assign(*op, target, value).await,
377
378            NodeKind::Call { callee, args, .. } => self.eval_call(callee, args).await,
379
380            NodeKind::MethodCall {
381                receiver,
382                method,
383                args,
384                ..
385            } => self.eval_method_call(receiver, &method.name.clone(), args).await,
386
387            NodeKind::FieldAccess { object, field } => {
388                self.eval_field_access(object, &field.name.clone()).await
389            }
390
391            NodeKind::Index { object, index } => self.eval_index(object, index).await,
392
393            NodeKind::Propagate { expr } => self.eval_propagate(expr).await,
394
395            NodeKind::Lambda { params, body } => self.eval_lambda(params, body),
396
397            NodeKind::Pipe { left, right } => self.eval_pipe(left, right).await,
398
399            NodeKind::Compose { left, right } => self.eval_compose(left, right).await,
400
401            // ── Collection literals ────────────────────────────────────────
402            NodeKind::ListLiteral { elems } => {
403                let mut values = Vec::with_capacity(elems.len());
404                for elem in elems {
405                    values.push(self.eval_expr(elem).await?);
406                }
407                Ok(Value::List(values))
408            }
409
410            NodeKind::MapLiteral { entries } => {
411                let mut map = BTreeMap::new();
412                for entry in entries {
413                    let k = self.eval_expr(&entry.key).await?;
414                    let v = self.eval_expr(&entry.value).await?;
415                    map.insert(k, v);
416                }
417                Ok(Value::Map(map))
418            }
419
420            NodeKind::SetLiteral { elems } => {
421                let mut set = BTreeSet::new();
422                for elem in elems {
423                    set.insert(self.eval_expr(elem).await?);
424                }
425                Ok(Value::Set(set))
426            }
427
428            NodeKind::TupleLiteral { elems } => {
429                let mut values = Vec::with_capacity(elems.len());
430                for elem in elems {
431                    values.push(self.eval_expr(elem).await?);
432                }
433                Ok(Value::Tuple(values))
434            }
435
436            NodeKind::RecordConstruct {
437                path,
438                fields,
439                spread,
440            } => self.eval_record_construct(path, fields, spread.as_deref()).await,
441
442            NodeKind::Interpolation { parts } => self.eval_interpolation(parts).await,
443
444            NodeKind::Range { lo, hi, inclusive } => self.eval_range(lo, hi, *inclusive).await,
445
446            NodeKind::ResultConstruct { variant, value } => {
447                let inner = match value {
448                    Some(v) => self.eval_expr(v).await?,
449                    None => Value::Void,
450                };
451                match variant {
452                    ResultVariant::Ok => Ok(Value::Result(Ok(Box::new(inner)))),
453                    ResultVariant::Err => Ok(Value::Result(Err(Box::new(inner)))),
454                }
455            }
456
457            // ── Control flow ───────────────────────────────────────────────
458            NodeKind::Block { stmts, tail } => self.eval_block(stmts, tail.as_deref()).await,
459
460            NodeKind::If {
461                let_pattern,
462                condition,
463                then_block,
464                else_block,
465            } => self.eval_if(
466                let_pattern.as_deref(),
467                condition,
468                then_block,
469                else_block.as_deref(),
470            ).await,
471
472            NodeKind::Match { scrutinee, arms } => self.eval_match(scrutinee, arms).await,
473
474            NodeKind::Return { value } => {
475                let v = match value {
476                    Some(e) => self.eval_expr(e).await?,
477                    None => Value::Void,
478                };
479                Err(RuntimeError::Return(Box::new(v)))
480            }
481
482            NodeKind::Break { value } => {
483                let v = match value {
484                    Some(e) => Some(Box::new(self.eval_expr(e).await?)),
485                    None => None,
486                };
487                Err(RuntimeError::Break(v))
488            }
489
490            NodeKind::Continue => Err(RuntimeError::Continue),
491
492            NodeKind::For {
493                pattern,
494                iterable,
495                body,
496            } => self.eval_for(pattern, iterable, body).await,
497
498            NodeKind::While { condition, body } => self.eval_while(condition, body).await,
499
500            NodeKind::Loop { body } => self.eval_loop(body).await,
501
502            NodeKind::Guard {
503                let_pattern,
504                condition,
505                else_block,
506            } => self.eval_guard(let_pattern.as_deref(), condition, else_block).await,
507
508            NodeKind::Unreachable => Err(RuntimeError::Unreachable),
509
510            // ── Ownership annotations (pass-through) ───────────────────────
511            NodeKind::Move { expr }
512            | NodeKind::Borrow { expr }
513            | NodeKind::MutableBorrow { expr } => self.eval_expr(expr).await,
514
515            // ── Async — `await` resolves a `Value::Future` to its inner value.
516            NodeKind::Await { expr } => {
517                let val = self.eval_expr(expr).await?;
518                match val {
519                    Value::Future(handle) => {
520                        let h = handle.lock().unwrap().take();
521                        match h {
522                            Some(jh) => match jh.await {
523                                Ok(inner) => inner,
524                                Err(e) => Err(RuntimeError::TypeError(format!(
525                                    "async task panicked: {e}"
526                                ))),
527                            },
528                            None => Err(RuntimeError::TypeError(
529                                "future already awaited".to_string(),
530                            )),
531                        }
532                    }
533                    other => Ok(other),
534                }
535            }
536
537            // ── Let binding (also appears as a statement inside blocks) ─────
538            NodeKind::LetBinding { pattern, value, .. } => {
539                let v = self.eval_expr(value).await?;
540                self.bind_pattern(pattern, v).await?;
541                Ok(Value::Void)
542            }
543
544            // ── Placeholder `_` ────────────────────────────────────────────
545            NodeKind::Placeholder => Ok(Value::Void),
546
547            // ── Effects ──────────────────────────────────────────────────────
548
549            // Effect declaration: register effect name, evaluate to Void.
550            NodeKind::EffectDecl { name, .. } => {
551                // Effect declarations are type-level; at runtime we just
552                // record the name so it can be referenced.
553                self.env.define(&name.name, Value::Void);
554                Ok(Value::Void)
555            }
556
557            // Module-level `handle Effect with handler`
558            NodeKind::ModuleHandle { effect, handler } => {
559                let handler_val = self.eval_expr(handler).await?;
560                let effect_name = self.type_path_to_name(effect);
561                self.effect_handlers
562                    .set_module_handler(effect_name, handler_val);
563                Ok(Value::Void)
564            }
565
566            // Effect operation invocation: resolve handler, call it.
567            NodeKind::EffectOp {
568                effect,
569                operation,
570                args,
571            } => {
572                let effect_name = self.type_path_to_name(effect);
573                let handler = self.effect_handlers.resolve(&effect_name).cloned().ok_or(
574                    RuntimeError::NoEffectHandler {
575                        effect: effect_name,
576                    },
577                )?;
578
579                // Evaluate arguments
580                let mut arg_values = Vec::with_capacity(args.len());
581                for arg in args {
582                    arg_values.push(self.eval_expr(&arg.value).await?);
583                }
584
585                // Call handler.operation(args...)
586                // The handler is a record value whose fields are the operation
587                // implementations, or a function if the effect has a single op.
588                self.dispatch_effect_op(&handler, &operation.name, arg_values).await
589            }
590
591            // Handling block: push handlers, execute body, pop handlers.
592            NodeKind::HandlingBlock { handlers, body } => {
593                let mut frame = std::collections::HashMap::new();
594                for pair in handlers {
595                    let handler_val = self.eval_expr(&pair.handler).await?;
596                    let effect_name = self.type_path_to_name(&pair.effect);
597                    frame.insert(effect_name, handler_val);
598                }
599                self.effect_handlers.push_handlers(frame);
600                let result = self.eval_expr(body).await;
601                self.effect_handlers.pop_handlers();
602                result
603            }
604
605            // Effect reference in type position — no runtime behavior.
606            NodeKind::EffectRef { .. } => Ok(Value::Void),
607
608            other => Err(RuntimeError::NotImplemented(
609                format!("{other:?}").chars().take(60).collect(),
610            )),
611        }
612    }
613
614    // ── Effect helpers ─────────────────────────────────────────────────────
615
616    /// Convert a `TypePath` to a dot-separated effect name string.
617    fn type_path_to_name(&self, tp: &TypePath) -> String {
618        tp.segments
619            .iter()
620            .map(|s| s.name.as_str())
621            .collect::<Vec<_>>()
622            .join(".")
623    }
624
625    /// Dispatch an effect operation call to a handler value.
626    ///
627    /// Resolution order for a record handler:
628    ///   1. A field on the record whose value is a function — call it
629    ///      with the operation arguments.
630    ///   2. An `impl EffectTrait for TypeName` method in the interpreter's
631    ///      method table — dispatched as a regular instance method.
632    ///
633    /// If the handler is a plain function, it's called directly (for
634    /// single-operation effects).
635    #[async_recursion]
636    async fn dispatch_effect_op(
637        &mut self,
638        handler: &Value,
639        operation: &str,
640        args: Vec<Value>,
641    ) -> Result<Value, RuntimeError> {
642        match handler {
643            // Record handler: look up operation as a field first, then
644            // fall back to impl-block methods.
645            Value::Record(rec) => {
646                if let Some(op_fn) = rec.fields.get(operation).cloned() {
647                    return self.call_fn_value(&op_fn, args).await;
648                }
649                if let Some(result) = self
650                    .try_call_impl_method(handler, operation, args)
651                    .await?
652                {
653                    return Ok(result);
654                }
655                Err(RuntimeError::FieldNotFound {
656                    field: operation.to_string(),
657                    type_name: rec.type_name.clone(),
658                })
659            }
660            // Function handler: single-operation effect, call directly
661            Value::Function(_) => {
662                let handler = handler.clone();
663                self.call_fn_value(&handler, args).await
664            }
665            other => Err(RuntimeError::TypeError(format!(
666                "effect handler must be a record or function, got {other}"
667            ))),
668        }
669    }
670
671    /// Call a `Value::Function` by its identity, looking up the closure in
672    /// the function registry.
673    /// Call a function value with the given arguments.
674    #[async_recursion]
675    pub async fn call_fn_value(&mut self, val: &Value, args: Vec<Value>) -> Result<Value, RuntimeError> {
676        let fn_id = match val {
677            Value::Function(fv) => fv.id,
678            other => {
679                return Err(RuntimeError::NotCallable {
680                    value: other.to_string(),
681                })
682            }
683        };
684        let closure =
685            self.fn_registry
686                .get(&fn_id)
687                .cloned()
688                .ok_or_else(|| RuntimeError::NotCallable {
689                    value: format!("unregistered fn #{fn_id}"),
690                })?;
691        self.call_closure(&closure, args).await
692    }
693
694    // ── Literal evaluation ─────────────────────────────────────────────────
695
696    fn eval_literal(&self, lit: &Literal) -> Result<Value, RuntimeError> {
697        match lit {
698            Literal::Int(s) => {
699                // Strip type suffix (e.g., _u8) before parsing.
700                let (numeric, _) = bock_ast::strip_type_suffix(s);
701                // Support 0x, 0o, 0b prefixes and optional _ separators.
702                let clean = numeric.replace('_', "");
703                let n = if clean.starts_with("0x") || clean.starts_with("0X") {
704                    i64::from_str_radix(&clean[2..], 16)
705                } else if clean.starts_with("0o") || clean.starts_with("0O") {
706                    i64::from_str_radix(&clean[2..], 8)
707                } else if clean.starts_with("0b") || clean.starts_with("0B") {
708                    i64::from_str_radix(&clean[2..], 2)
709                } else {
710                    clean.parse::<i64>()
711                };
712                n.map(Value::Int)
713                    .map_err(|_| RuntimeError::IntParseFailed(s.clone()))
714            }
715            Literal::Float(s) => {
716                // Strip type suffix (e.g., _f32) before parsing.
717                let (numeric, _) = bock_ast::strip_type_suffix(s);
718                numeric
719                    .replace('_', "")
720                    .parse::<f64>()
721                    .map(|f| Value::Float(OrdF64(f)))
722                    .map_err(|_| RuntimeError::FloatParseFailed(s.clone()))
723            }
724            Literal::Bool(b) => Ok(Value::Bool(*b)),
725            Literal::Char(s) => Ok(Value::Char(s.chars().next().unwrap_or('\0'))),
726            Literal::String(s) => Ok(Value::String(BockString::new(s.clone()))),
727            Literal::Unit => Ok(Value::Void),
728        }
729    }
730
731    // ── Binary operator evaluation ─────────────────────────────────────────
732
733    #[async_recursion]
734    async fn eval_binary_op(
735        &mut self,
736        op: BinOp,
737        left: &AIRNode,
738        right: &AIRNode,
739    ) -> Result<Value, RuntimeError> {
740        // Short-circuit logical operators.
741        match op {
742            BinOp::And => {
743                let l = self.eval_expr(left).await?;
744                return match l {
745                    Value::Bool(false) => Ok(Value::Bool(false)),
746                    Value::Bool(true) => self.eval_expr(right).await,
747                    other => Err(RuntimeError::TypeError(format!(
748                        "expected Bool in &&, got {other}"
749                    ))),
750                };
751            }
752            BinOp::Or => {
753                let l = self.eval_expr(left).await?;
754                return match l {
755                    Value::Bool(true) => Ok(Value::Bool(true)),
756                    Value::Bool(false) => self.eval_expr(right).await,
757                    other => Err(RuntimeError::TypeError(format!(
758                        "expected Bool in ||, got {other}"
759                    ))),
760                };
761            }
762            _ => {}
763        }
764
765        let l = self.eval_expr(left).await?;
766        let r = self.eval_expr(right).await?;
767
768        match (op, l, r) {
769            // ── Arithmetic ────────────────────────────────────────────────
770            (BinOp::Add, Value::Int(a), Value::Int(b)) => a
771                .checked_add(b)
772                .map(Value::Int)
773                .ok_or(RuntimeError::IntOverflow),
774            (BinOp::Add, Value::Float(a), Value::Float(b)) => Ok(Value::Float(OrdF64(a.0 + b.0))),
775            (BinOp::Add, Value::String(a), Value::String(b)) => {
776                Ok(Value::String(BockString::new(format!("{a}{b}"))))
777            }
778
779            (BinOp::Sub, Value::Int(a), Value::Int(b)) => a
780                .checked_sub(b)
781                .map(Value::Int)
782                .ok_or(RuntimeError::IntOverflow),
783            (BinOp::Sub, Value::Float(a), Value::Float(b)) => Ok(Value::Float(OrdF64(a.0 - b.0))),
784
785            (BinOp::Mul, Value::Int(a), Value::Int(b)) => a
786                .checked_mul(b)
787                .map(Value::Int)
788                .ok_or(RuntimeError::IntOverflow),
789            (BinOp::Mul, Value::Float(a), Value::Float(b)) => Ok(Value::Float(OrdF64(a.0 * b.0))),
790
791            (BinOp::Div, Value::Int(a), Value::Int(b)) => {
792                if b == 0 {
793                    Err(RuntimeError::DivisionByZero)
794                } else {
795                    Ok(Value::Int(a / b))
796                }
797            }
798            (BinOp::Div, Value::Float(a), Value::Float(b)) => Ok(Value::Float(OrdF64(a.0 / b.0))),
799
800            (BinOp::Rem, Value::Int(a), Value::Int(b)) => {
801                if b == 0 {
802                    Err(RuntimeError::DivisionByZero)
803                } else {
804                    Ok(Value::Int(a % b))
805                }
806            }
807            (BinOp::Rem, Value::Float(a), Value::Float(b)) => Ok(Value::Float(OrdF64(a.0 % b.0))),
808
809            (BinOp::Pow, Value::Int(a), Value::Int(b)) => {
810                if b < 0 {
811                    Err(RuntimeError::TypeError(
812                        "negative integer exponent".to_string(),
813                    ))
814                } else if b > u32::MAX as i64 {
815                    Err(RuntimeError::IntOverflow)
816                } else {
817                    a.checked_pow(b as u32)
818                        .map(Value::Int)
819                        .ok_or(RuntimeError::IntOverflow)
820                }
821            }
822            (BinOp::Pow, Value::Float(a), Value::Float(b)) => {
823                Ok(Value::Float(OrdF64(a.0.powf(b.0))))
824            }
825
826            // ── Comparison ────────────────────────────────────────────────
827            (BinOp::Eq, l, r) => Ok(Value::Bool(l == r)),
828            (BinOp::Ne, l, r) => Ok(Value::Bool(l != r)),
829            (BinOp::Lt, l, r) => Ok(Value::Bool(l < r)),
830            (BinOp::Le, l, r) => Ok(Value::Bool(l <= r)),
831            (BinOp::Gt, l, r) => Ok(Value::Bool(l > r)),
832            (BinOp::Ge, l, r) => Ok(Value::Bool(l >= r)),
833
834            // ── Bitwise ───────────────────────────────────────────────────
835            (BinOp::BitAnd, Value::Int(a), Value::Int(b)) => Ok(Value::Int(a & b)),
836            (BinOp::BitOr, Value::Int(a), Value::Int(b)) => Ok(Value::Int(a | b)),
837            (BinOp::BitXor, Value::Int(a), Value::Int(b)) => Ok(Value::Int(a ^ b)),
838            // ── Function composition (`>>`) ────────────────────────────────
839            // Note: Compose as a BinOp is separate from NodeKind::Compose.
840            (BinOp::Compose, Value::Function(f), Value::Function(g)) => {
841                let fn_val = FnValue::new_anonymous();
842                let id = fn_val.id;
843                self.fn_registry.insert(
844                    id,
845                    Closure {
846                        params: vec!["__x".to_string()],
847                        body: ClosureBody::Composed {
848                            inner: f.id,
849                            outer: g.id,
850                        },
851                        captured: self.env.clone(),
852                        is_toplevel: false,
853                is_async: false,
854                    },
855                );
856                Ok(Value::Function(fn_val))
857            }
858
859            // ── Is (type check) ───────────────────────────────────────────
860            (BinOp::Is, value, Value::String(type_name)) => {
861                let tag = TypeTag::of(&value);
862                Ok(Value::Bool(tag.name() == type_name.as_str()))
863            }
864
865            // ── Trait dispatch fallback ────────────────────────────────────
866            // If the inline handler didn't match, try dispatching via
867            // registered trait methods (e.g., Add.add, Comparable.compare).
868            (op, l, r) => {
869                let tag = TypeTag::of(&l);
870                let method = match op {
871                    BinOp::Add => Some("add"),
872                    BinOp::Sub => Some("sub"),
873                    BinOp::Mul => Some("mul"),
874                    BinOp::Div => Some("div"),
875                    BinOp::Rem => Some("rem"),
876                    BinOp::Pow => Some("pow"),
877                    _ => None,
878                };
879                if let Some(name) = method {
880                    if let Some(result) = self.builtins.call(tag, name, &[l.clone(), r.clone()]) {
881                        return result;
882                    }
883                }
884                Err(RuntimeError::TypeError(format!(
885                    "operator {op:?} not supported for {l} and {r}"
886                )))
887            }
888        }
889    }
890
891    // ── Unary operator evaluation ──────────────────────────────────────────
892
893    #[async_recursion]
894    async fn eval_unary_op(&mut self, op: UnaryOp, operand: &AIRNode) -> Result<Value, RuntimeError> {
895        let val = self.eval_expr(operand).await?;
896        match (op, val) {
897            (UnaryOp::Neg, Value::Int(n)) => n
898                .checked_neg()
899                .map(Value::Int)
900                .ok_or(RuntimeError::IntOverflow),
901            (UnaryOp::Neg, Value::Float(f)) => Ok(Value::Float(OrdF64(-f.0))),
902            (UnaryOp::Not, Value::Bool(b)) => Ok(Value::Bool(!b)),
903            (UnaryOp::BitNot, Value::Int(n)) => Ok(Value::Int(!n)),
904            (op, val) => Err(RuntimeError::TypeError(format!(
905                "unary operator {op:?} not supported for {val}"
906            ))),
907        }
908    }
909
910    // ── Assignment ─────────────────────────────────────────────────────────
911
912    #[async_recursion]
913    async fn eval_assign(
914        &mut self,
915        op: AssignOp,
916        target: &AIRNode,
917        value: &AIRNode,
918    ) -> Result<Value, RuntimeError> {
919        let rhs = self.eval_expr(value).await?;
920        match &target.kind {
921            NodeKind::Identifier { name } => {
922                let name = name.name.clone();
923                let new_val = match op {
924                    AssignOp::Assign => rhs,
925                    compound => {
926                        let current = self.env.get(&name).cloned().ok_or_else(|| {
927                            RuntimeError::UndefinedVariable { name: name.clone() }
928                        })?;
929                        self.apply_assign_op(compound, current, rhs)?
930                    }
931                };
932                if !self.env.assign(&name, new_val) {
933                    return Err(RuntimeError::UndefinedVariable { name });
934                }
935                Ok(Value::Void)
936            }
937            NodeKind::FieldAccess { object, field } => {
938                let field_name = field.name.clone();
939                let obj = self.eval_expr(object).await?;
940                match obj {
941                    Value::Record(mut rv) => {
942                        let new_val = match op {
943                            AssignOp::Assign => rhs,
944                            compound => {
945                                let current =
946                                    rv.fields.get(&field_name).cloned().ok_or_else(|| {
947                                        RuntimeError::FieldNotFound {
948                                            field: field_name.clone(),
949                                            type_name: rv.type_name.clone(),
950                                        }
951                                    })?;
952                                self.apply_assign_op(compound, current, rhs)?
953                            }
954                        };
955                        rv.fields.insert(field_name, new_val);
956                        // Re-bind the object in the environment
957                        if let NodeKind::Identifier { name: obj_name } = &object.kind {
958                            let updated = Value::Record(rv);
959                            if !self.env.assign(&obj_name.name, updated) {
960                                return Err(RuntimeError::UndefinedVariable {
961                                    name: obj_name.name.clone(),
962                                });
963                            }
964                        }
965                        Ok(Value::Void)
966                    }
967                    other => Err(RuntimeError::TypeError(format!(
968                        "cannot assign to field '{field_name}' on {other}"
969                    ))),
970                }
971            }
972            NodeKind::Index { object, index } => {
973                let idx = self.eval_expr(index).await?;
974                let obj = self.eval_expr(object).await?;
975                match (obj, idx) {
976                    (Value::List(mut items), Value::Int(i)) => {
977                        if i < 0 || i as usize >= items.len() {
978                            return Err(RuntimeError::IndexOutOfBounds {
979                                index: i,
980                                len: items.len(),
981                            });
982                        }
983                        let new_val = match op {
984                            AssignOp::Assign => rhs,
985                            compound => {
986                                let current = items[i as usize].clone();
987                                self.apply_assign_op(compound, current, rhs)?
988                            }
989                        };
990                        items[i as usize] = new_val;
991                        if let NodeKind::Identifier { name: obj_name } = &object.kind {
992                            let updated = Value::List(items);
993                            if !self.env.assign(&obj_name.name, updated) {
994                                return Err(RuntimeError::UndefinedVariable {
995                                    name: obj_name.name.clone(),
996                                });
997                            }
998                        }
999                        Ok(Value::Void)
1000                    }
1001                    (Value::Map(mut map), key) => {
1002                        let new_val = match op {
1003                            AssignOp::Assign => rhs,
1004                            compound => {
1005                                let current = map.get(&key).cloned().ok_or_else(|| {
1006                                    RuntimeError::TypeError(format!("key not found: {key}"))
1007                                })?;
1008                                self.apply_assign_op(compound, current, rhs)?
1009                            }
1010                        };
1011                        map.insert(key, new_val);
1012                        if let NodeKind::Identifier { name: obj_name } = &object.kind {
1013                            let updated = Value::Map(map);
1014                            if !self.env.assign(&obj_name.name, updated) {
1015                                return Err(RuntimeError::UndefinedVariable {
1016                                    name: obj_name.name.clone(),
1017                                });
1018                            }
1019                        }
1020                        Ok(Value::Void)
1021                    }
1022                    (obj, idx) => Err(RuntimeError::TypeError(format!(
1023                        "cannot index-assign {obj} with {idx}"
1024                    ))),
1025                }
1026            }
1027            _ => Err(RuntimeError::NotImplemented(
1028                "unsupported assignment target".to_string(),
1029            )),
1030        }
1031    }
1032
1033    fn apply_assign_op(&self, op: AssignOp, lhs: Value, rhs: Value) -> Result<Value, RuntimeError> {
1034        match (op, lhs, rhs) {
1035            (AssignOp::AddAssign, Value::Int(a), Value::Int(b)) => a
1036                .checked_add(b)
1037                .map(Value::Int)
1038                .ok_or(RuntimeError::IntOverflow),
1039            (AssignOp::SubAssign, Value::Int(a), Value::Int(b)) => a
1040                .checked_sub(b)
1041                .map(Value::Int)
1042                .ok_or(RuntimeError::IntOverflow),
1043            (AssignOp::MulAssign, Value::Int(a), Value::Int(b)) => a
1044                .checked_mul(b)
1045                .map(Value::Int)
1046                .ok_or(RuntimeError::IntOverflow),
1047            (AssignOp::DivAssign, Value::Int(a), Value::Int(b)) => {
1048                if b == 0 {
1049                    Err(RuntimeError::DivisionByZero)
1050                } else {
1051                    Ok(Value::Int(a / b))
1052                }
1053            }
1054            (AssignOp::RemAssign, Value::Int(a), Value::Int(b)) => {
1055                if b == 0 {
1056                    Err(RuntimeError::DivisionByZero)
1057                } else {
1058                    Ok(Value::Int(a % b))
1059                }
1060            }
1061            (AssignOp::AddAssign, Value::Float(a), Value::Float(b)) => {
1062                Ok(Value::Float(OrdF64(a.0 + b.0)))
1063            }
1064            (AssignOp::SubAssign, Value::Float(a), Value::Float(b)) => {
1065                Ok(Value::Float(OrdF64(a.0 - b.0)))
1066            }
1067            (AssignOp::MulAssign, Value::Float(a), Value::Float(b)) => {
1068                Ok(Value::Float(OrdF64(a.0 * b.0)))
1069            }
1070            (AssignOp::DivAssign, Value::Float(a), Value::Float(b)) => {
1071                Ok(Value::Float(OrdF64(a.0 / b.0)))
1072            }
1073            (AssignOp::AddAssign, Value::String(a), Value::String(b)) => {
1074                Ok(Value::String(BockString::new(format!("{a}{b}"))))
1075            }
1076            (op, l, r) => Err(RuntimeError::TypeError(format!(
1077                "compound assignment {op:?} not supported for {l} and {r}"
1078            ))),
1079        }
1080    }
1081
1082    // ── Function calls ─────────────────────────────────────────────────────
1083
1084    #[async_recursion]
1085    async fn eval_call(&mut self, callee: &AIRNode, args: &[AirArg]) -> Result<Value, RuntimeError> {
1086        // Check for global built-in functions first (e.g., print, println, debug).
1087        if let NodeKind::Identifier { name } = &callee.kind {
1088            if self.builtins.has_global(&name.name) {
1089                let mut arg_values: Vec<Value> = Vec::with_capacity(args.len());
1090                for a in args {
1091                    arg_values.push(self.eval_expr(&a.value).await?);
1092                }
1093                return self
1094                    .builtins
1095                    .call_global(&name.name, &arg_values)
1096                    .expect("has_global check confirmed builtin exists");
1097            }
1098
1099            // Check for effect operation dispatch: log(msg) → Logger handler
1100            if let Some(effect_name) = self.effect_operations.get(&name.name).cloned() {
1101                let handler =
1102                    self.effect_handlers
1103                        .resolve(&effect_name)
1104                        .cloned()
1105                        .ok_or(RuntimeError::NoEffectHandler {
1106                            effect: effect_name,
1107                        })?;
1108                let mut arg_values = Vec::with_capacity(args.len());
1109                for arg in args {
1110                    arg_values.push(self.eval_expr(&arg.value).await?);
1111                }
1112                return self.dispatch_effect_op(&handler, &name.name, arg_values).await;
1113            }
1114        }
1115
1116        // Check for associated function calls: `Type.method(args)` is lowered
1117        // to `Call(FieldAccess(Type, method), args)` with NO self prepended.
1118        // Associated functions are registered as qualified globals like
1119        // `Duration.seconds`; detect this before the desugared-method path.
1120        if let NodeKind::FieldAccess { object, field } = &callee.kind {
1121            if let NodeKind::Identifier { name: type_name } = &object.kind {
1122                let qualified = format!("{}.{}", type_name.name, field.name);
1123                if self.builtins.has_global(&qualified) {
1124                    let mut arg_values = Vec::with_capacity(args.len());
1125                    for a in args {
1126                        arg_values.push(self.eval_expr(&a.value).await?);
1127                    }
1128                    return self
1129                        .builtins
1130                        .call_global(&qualified, &arg_values)
1131                        .expect("has_global check confirmed builtin exists");
1132                }
1133            }
1134        }
1135
1136        // Check for desugared method calls: the lowerer converts `obj.method(args)`
1137        // into `Call(FieldAccess(obj, method), [obj, ...args])`. Try builtin dispatch
1138        // before falling through to field-access evaluation.
1139        if let NodeKind::FieldAccess { object, field } = &callee.kind {
1140            let recv = self.eval_expr(object).await?;
1141            let type_tag = TypeTag::of(&recv);
1142            if self.builtins.has_method(type_tag, &field.name) {
1143                let mut builtin_args = Vec::with_capacity(args.len());
1144                builtin_args.push(recv);
1145                // Skip the first arg (self/receiver inserted by lowerer)
1146                for a in args.iter().skip(1) {
1147                    builtin_args.push(self.eval_expr(&a.value).await?);
1148                }
1149                // Try higher-order builtin first (needs callback invoker)
1150                if let Some(ho_func) = self.builtins.get_ho_method(type_tag, &field.name) {
1151                    return ho_func(&builtin_args, self).await;
1152                }
1153                return self
1154                    .builtins
1155                    .call(type_tag, &field.name, &builtin_args)
1156                    .expect("has_method check confirmed builtin exists");
1157            }
1158            // Try user-defined impl methods (skip first arg which is receiver duplicate).
1159            let mut method_args = Vec::with_capacity(args.len().saturating_sub(1));
1160            for a in args.iter().skip(1) {
1161                method_args.push(self.eval_expr(&a.value).await?);
1162            }
1163            if let Some(result) =
1164                self.try_call_impl_method(&recv, &field.name, method_args).await?
1165            {
1166                return Ok(result);
1167            }
1168            // Fallback: try inline collection method dispatch (contains, map,
1169            // filter, keys, first, etc.) which lives in eval_method_call.
1170            return self.eval_method_call(object, &field.name, &args[1..]).await;
1171        }
1172
1173        let fn_val = self.eval_expr(callee).await?;
1174        let fn_id = match &fn_val {
1175            Value::Function(fv) => fv.id,
1176            other => {
1177                return Err(RuntimeError::NotCallable {
1178                    value: other.to_string(),
1179                })
1180            }
1181        };
1182        let mut arg_values: Vec<Value> = Vec::with_capacity(args.len());
1183                for a in args {
1184                    arg_values.push(self.eval_expr(&a.value).await?);
1185                }
1186        let closure =
1187            self.fn_registry
1188                .get(&fn_id)
1189                .cloned()
1190                .ok_or_else(|| RuntimeError::NotCallable {
1191                    value: format!("unregistered fn #{fn_id}"),
1192                })?;
1193        self.call_closure(&closure, arg_values).await
1194    }
1195
1196    #[async_recursion]
1197    async fn call_closure(&mut self, closure: &Closure, args: Vec<Value>) -> Result<Value, RuntimeError> {
1198        match &closure.body {
1199            ClosureBody::Air(body) => {
1200                if closure.params.len() != args.len() {
1201                    return Err(RuntimeError::ArityMismatch {
1202                        expected: closure.params.len(),
1203                        got: args.len(),
1204                    });
1205                }
1206                let body = *body.clone();
1207
1208                if closure.is_async {
1209                    // Spawn an async task with a CLONE of the interpreter so it
1210                    // owns its env, fn_registry, etc. for the duration of the
1211                    // call. The current interpreter resumes immediately with a
1212                    // `Value::Future` that resolves when the task completes.
1213                    let mut sub = self.clone();
1214                    if !closure.is_toplevel {
1215                        sub.env = closure.captured.clone();
1216                    }
1217                    sub.env.push_scope();
1218                    for (name, val) in closure.params.iter().zip(args) {
1219                        sub.env.define(name.clone(), val);
1220                    }
1221                    let handle: tokio::task::JoinHandle<Result<Value, RuntimeError>> =
1222                        tokio::spawn(async move {
1223                            match sub.eval_expr(&body).await {
1224                                Err(RuntimeError::Return(v)) => Ok(*v),
1225                                other => other,
1226                            }
1227                        });
1228                    return Ok(Value::Future(Arc::new(Mutex::new(Some(handle)))));
1229                }
1230
1231                // Top-level functions use the current env (all globals visible);
1232                // closures/lambdas use their captured env (lexical scoping).
1233                let saved_env = if closure.is_toplevel {
1234                    self.env.clone()
1235                } else {
1236                    std::mem::replace(&mut self.env, closure.captured.clone())
1237                };
1238                self.env.push_scope();
1239                for (name, val) in closure.params.iter().zip(args) {
1240                    self.env.define(name.clone(), val);
1241                }
1242                let result = self.eval_expr(&body).await;
1243                self.env = saved_env;
1244                // Convert a `return` signal into the return value.
1245                match result {
1246                    Err(RuntimeError::Return(v)) => Ok(*v),
1247                    other => other,
1248                }
1249            }
1250            ClosureBody::Composed { inner, outer } => {
1251                let inner_id = *inner;
1252                let outer_id = *outer;
1253                let inner_closure = self.fn_registry.get(&inner_id).cloned().ok_or_else(|| {
1254                    RuntimeError::NotCallable {
1255                        value: format!("composed inner fn #{inner_id}"),
1256                    }
1257                })?;
1258                let intermediate = self.call_closure(&inner_closure, args).await?;
1259                let outer_closure = self.fn_registry.get(&outer_id).cloned().ok_or_else(|| {
1260                    RuntimeError::NotCallable {
1261                        value: format!("composed outer fn #{outer_id}"),
1262                    }
1263                })?;
1264                self.call_closure(&outer_closure, vec![intermediate]).await
1265            }
1266            ClosureBody::Native(build) => {
1267                if closure.params.len() != args.len() {
1268                    return Err(RuntimeError::ArityMismatch {
1269                        expected: closure.params.len(),
1270                        got: args.len(),
1271                    });
1272                }
1273                Ok(build(&args))
1274            }
1275        }
1276    }
1277
1278    // ── Lambda / closure creation ──────────────────────────────────────────
1279
1280    fn eval_lambda(&mut self, params: &[AIRNode], body: &AIRNode) -> Result<Value, RuntimeError> {
1281        let param_names: Vec<String> = params
1282            .iter()
1283            .map(|p| match &p.kind {
1284                NodeKind::Param { pattern, .. } => match &pattern.kind {
1285                    NodeKind::BindPat { name, .. } => name.name.clone(),
1286                    NodeKind::WildcardPat => "_".to_string(),
1287                    _ => "_".to_string(),
1288                },
1289                _ => "_".to_string(),
1290            })
1291            .collect();
1292
1293        let fn_val = FnValue::new_anonymous();
1294        let id = fn_val.id;
1295        let captured = self.env.clone();
1296        self.fn_registry.insert(
1297            id,
1298            Closure {
1299                params: param_names,
1300                body: ClosureBody::Air(Box::new(body.clone())),
1301                captured,
1302                is_toplevel: false,
1303                is_async: false,
1304            },
1305        );
1306        Ok(Value::Function(fn_val))
1307    }
1308
1309    // ── Pipe operator ──────────────────────────────────────────────────────
1310
1311    #[async_recursion]
1312    async fn eval_pipe(&mut self, left: &AIRNode, right: &AIRNode) -> Result<Value, RuntimeError> {
1313        let lhs = self.eval_expr(left).await?;
1314        // If the right-hand side is itself a call, prepend lhs to its args.
1315        match &right.kind.clone() {
1316            NodeKind::Call { callee, args, .. } => {
1317                let callee = callee.clone();
1318                let args: Vec<AirArg> = args.clone();
1319                let fn_val = self.eval_expr(&callee).await?;
1320                let fn_id = match &fn_val {
1321                    Value::Function(fv) => fv.id,
1322                    other => {
1323                        return Err(RuntimeError::NotCallable {
1324                            value: other.to_string(),
1325                        })
1326                    }
1327                };
1328                let mut arg_values = vec![lhs];
1329                for a in &args {
1330                    arg_values.push(self.eval_expr(&a.value).await?);
1331                }
1332                let closure = self.fn_registry.get(&fn_id).cloned().ok_or_else(|| {
1333                    RuntimeError::NotCallable {
1334                        value: format!("unregistered fn #{fn_id}"),
1335                    }
1336                })?;
1337                self.call_closure(&closure, arg_values).await
1338            }
1339            _ => {
1340                // Evaluate right as a function, pass lhs as single argument.
1341                let fn_val = self.eval_expr(right).await?;
1342                let fn_id = match &fn_val {
1343                    Value::Function(fv) => fv.id,
1344                    other => {
1345                        return Err(RuntimeError::NotCallable {
1346                            value: other.to_string(),
1347                        })
1348                    }
1349                };
1350                let closure = self.fn_registry.get(&fn_id).cloned().ok_or_else(|| {
1351                    RuntimeError::NotCallable {
1352                        value: format!("unregistered fn #{fn_id}"),
1353                    }
1354                })?;
1355                self.call_closure(&closure, vec![lhs]).await
1356            }
1357        }
1358    }
1359
1360    // ── Function composition ───────────────────────────────────────────────
1361
1362    #[async_recursion]
1363    async fn eval_compose(&mut self, left: &AIRNode, right: &AIRNode) -> Result<Value, RuntimeError> {
1364        let f = self.eval_expr(left).await?;
1365        let g = self.eval_expr(right).await?;
1366        let f_id = match &f {
1367            Value::Function(fv) => fv.id,
1368            other => {
1369                return Err(RuntimeError::TypeError(format!(
1370                    ">> requires functions, got {other}"
1371                )))
1372            }
1373        };
1374        let g_id = match &g {
1375            Value::Function(fv) => fv.id,
1376            other => {
1377                return Err(RuntimeError::TypeError(format!(
1378                    ">> requires functions, got {other}"
1379                )))
1380            }
1381        };
1382        let fn_val = FnValue::new_anonymous();
1383        let id = fn_val.id;
1384        self.fn_registry.insert(
1385            id,
1386            Closure {
1387                params: vec!["__x".to_string()],
1388                body: ClosureBody::Composed {
1389                    inner: f_id,
1390                    outer: g_id,
1391                },
1392                captured: self.env.clone(),
1393                is_toplevel: false,
1394                is_async: false,
1395            },
1396        );
1397        Ok(Value::Function(fn_val))
1398    }
1399
1400    // ── Method calls ───────────────────────────────────────────────────────
1401
1402    #[async_recursion]
1403    async fn eval_method_call(
1404        &mut self,
1405        receiver: &AIRNode,
1406        method: &str,
1407        args: &[AirArg],
1408    ) -> Result<Value, RuntimeError> {
1409        let recv = self.eval_expr(receiver).await?;
1410        let method = method.to_string();
1411        let mut arg_values: Vec<Value> = Vec::with_capacity(args.len());
1412        for a in args {
1413            arg_values.push(self.eval_expr(&a.value).await?);
1414        }
1415
1416        // Check the builtin registry first.
1417        let type_tag = TypeTag::of(&recv);
1418        {
1419            let mut builtin_args = Vec::with_capacity(1 + arg_values.len());
1420            builtin_args.push(recv.clone());
1421            builtin_args.extend(arg_values.iter().cloned());
1422            // Try higher-order builtin first (needs callback invoker)
1423            if let Some(ho_func) = self.builtins.get_ho_method(type_tag, &method) {
1424                return ho_func(&builtin_args, self).await;
1425            }
1426            if let Some(result) = self.builtins.call(type_tag, &method, &builtin_args) {
1427                return result;
1428            }
1429        }
1430
1431        match (&recv, method.as_str()) {
1432            // ── Universal ─────────────────────────────────────────────────
1433            (_, "to_string") => Ok(Value::String(BockString::new(recv.to_string()))),
1434
1435            // ── List ──────────────────────────────────────────────────────
1436            (Value::List(items), "len") => Ok(Value::Int(items.len() as i64)),
1437            (Value::List(items), "is_empty") => Ok(Value::Bool(items.is_empty())),
1438            (Value::List(items), "first") => {
1439                Ok(Value::Optional(items.first().cloned().map(Box::new)))
1440            }
1441            (Value::List(items), "last") => {
1442                Ok(Value::Optional(items.last().cloned().map(Box::new)))
1443            }
1444            (Value::List(items), "get") => {
1445                let idx = arg_values.first().ok_or(RuntimeError::ArityMismatch {
1446                    expected: 1,
1447                    got: 0,
1448                })?;
1449                if let Value::Int(i) = idx {
1450                    let i = *i;
1451                    if i < 0 || i as usize >= items.len() {
1452                        Ok(Value::Optional(None))
1453                    } else {
1454                        Ok(Value::Optional(Some(Box::new(items[i as usize].clone()))))
1455                    }
1456                } else {
1457                    Err(RuntimeError::TypeError(
1458                        "List.get expects an Int index".to_string(),
1459                    ))
1460                }
1461            }
1462            (Value::List(items), "push") => {
1463                let v = arg_values
1464                    .into_iter()
1465                    .next()
1466                    .ok_or(RuntimeError::ArityMismatch {
1467                        expected: 1,
1468                        got: 0,
1469                    })?;
1470                let mut new_list = items.clone();
1471                new_list.push(v);
1472                Ok(Value::List(new_list))
1473            }
1474            (Value::List(items), "contains") => {
1475                let v = arg_values.first().ok_or(RuntimeError::ArityMismatch {
1476                    expected: 1,
1477                    got: 0,
1478                })?;
1479                Ok(Value::Bool(items.contains(v)))
1480            }
1481            (Value::List(items), "reverse") => {
1482                let mut v = items.clone();
1483                v.reverse();
1484                Ok(Value::List(v))
1485            }
1486            (Value::List(items), "map") => {
1487                let fn_val = arg_values
1488                    .into_iter()
1489                    .next()
1490                    .ok_or(RuntimeError::ArityMismatch {
1491                        expected: 1,
1492                        got: 0,
1493                    })?;
1494                let fn_id = match fn_val {
1495                    Value::Function(ref fv) => fv.id,
1496                    other => {
1497                        return Err(RuntimeError::TypeError(format!(
1498                            "List.map expects a function, got {other}"
1499                        )))
1500                    }
1501                };
1502                let closure = self.fn_registry.get(&fn_id).cloned().ok_or_else(|| {
1503                    RuntimeError::NotCallable {
1504                        value: "unregistered fn".to_string(),
1505                    }
1506                })?;
1507                let items = items.clone();
1508                let mut result = Vec::with_capacity(items.len());
1509                for item in items {
1510                    result.push(self.call_closure(&closure, vec![item]).await?);
1511                }
1512                Ok(Value::List(result))
1513            }
1514            (Value::List(items), "filter") => {
1515                let fn_val = arg_values
1516                    .into_iter()
1517                    .next()
1518                    .ok_or(RuntimeError::ArityMismatch {
1519                        expected: 1,
1520                        got: 0,
1521                    })?;
1522                let fn_id = match fn_val {
1523                    Value::Function(ref fv) => fv.id,
1524                    other => {
1525                        return Err(RuntimeError::TypeError(format!(
1526                            "List.filter expects a function, got {other}"
1527                        )))
1528                    }
1529                };
1530                let closure = self.fn_registry.get(&fn_id).cloned().ok_or_else(|| {
1531                    RuntimeError::NotCallable {
1532                        value: "unregistered fn".to_string(),
1533                    }
1534                })?;
1535                let items = items.clone();
1536                let mut result = Vec::new();
1537                for item in items {
1538                    if let Value::Bool(true) = self.call_closure(&closure, vec![item.clone()]).await? {
1539                        result.push(item);
1540                    }
1541                }
1542                Ok(Value::List(result))
1543            }
1544            (Value::List(items), "fold") | (Value::List(items), "reduce") => {
1545                if arg_values.len() != 2 {
1546                    return Err(RuntimeError::ArityMismatch {
1547                        expected: 2,
1548                        got: arg_values.len(),
1549                    });
1550                }
1551                let mut acc = arg_values.remove(0);
1552                let fn_val = arg_values.remove(0);
1553                let fn_id = match fn_val {
1554                    Value::Function(ref fv) => fv.id,
1555                    other => {
1556                        return Err(RuntimeError::TypeError(format!(
1557                            "List.fold expects a function, got {other}"
1558                        )))
1559                    }
1560                };
1561                let closure = self.fn_registry.get(&fn_id).cloned().ok_or_else(|| {
1562                    RuntimeError::NotCallable {
1563                        value: "unregistered fn".to_string(),
1564                    }
1565                })?;
1566                let items = items.clone();
1567                for item in items {
1568                    acc = self.call_closure(&closure, vec![acc, item]).await?;
1569                }
1570                Ok(acc)
1571            }
1572            (Value::List(items), "any") => {
1573                let fn_val = arg_values
1574                    .into_iter()
1575                    .next()
1576                    .ok_or(RuntimeError::ArityMismatch {
1577                        expected: 1,
1578                        got: 0,
1579                    })?;
1580                let fn_id = match fn_val {
1581                    Value::Function(ref fv) => fv.id,
1582                    other => {
1583                        return Err(RuntimeError::TypeError(format!(
1584                            "List.any expects a function, got {other}"
1585                        )))
1586                    }
1587                };
1588                let closure = self.fn_registry.get(&fn_id).cloned().ok_or_else(|| {
1589                    RuntimeError::NotCallable {
1590                        value: "unregistered fn".to_string(),
1591                    }
1592                })?;
1593                let items = items.clone();
1594                for item in items {
1595                    if let Value::Bool(true) = self.call_closure(&closure, vec![item]).await? {
1596                        return Ok(Value::Bool(true));
1597                    }
1598                }
1599                Ok(Value::Bool(false))
1600            }
1601            (Value::List(items), "all") => {
1602                let fn_val = arg_values
1603                    .into_iter()
1604                    .next()
1605                    .ok_or(RuntimeError::ArityMismatch {
1606                        expected: 1,
1607                        got: 0,
1608                    })?;
1609                let fn_id = match fn_val {
1610                    Value::Function(ref fv) => fv.id,
1611                    other => {
1612                        return Err(RuntimeError::TypeError(format!(
1613                            "List.all expects a function, got {other}"
1614                        )))
1615                    }
1616                };
1617                let closure = self.fn_registry.get(&fn_id).cloned().ok_or_else(|| {
1618                    RuntimeError::NotCallable {
1619                        value: "unregistered fn".to_string(),
1620                    }
1621                })?;
1622                let items = items.clone();
1623                for item in items {
1624                    if let Value::Bool(false) = self.call_closure(&closure, vec![item]).await? {
1625                        return Ok(Value::Bool(false));
1626                    }
1627                }
1628                Ok(Value::Bool(true))
1629            }
1630            (Value::List(items), "find") => {
1631                let fn_val = arg_values
1632                    .into_iter()
1633                    .next()
1634                    .ok_or(RuntimeError::ArityMismatch {
1635                        expected: 1,
1636                        got: 0,
1637                    })?;
1638                let fn_id = match fn_val {
1639                    Value::Function(ref fv) => fv.id,
1640                    other => {
1641                        return Err(RuntimeError::TypeError(format!(
1642                            "List.find expects a function, got {other}"
1643                        )))
1644                    }
1645                };
1646                let closure = self.fn_registry.get(&fn_id).cloned().ok_or_else(|| {
1647                    RuntimeError::NotCallable {
1648                        value: "unregistered fn".to_string(),
1649                    }
1650                })?;
1651                let items = items.clone();
1652                for item in items {
1653                    if let Value::Bool(true) =
1654                        self.call_closure(&closure, vec![item.clone()]).await?
1655                    {
1656                        return Ok(Value::Optional(Some(Box::new(item))));
1657                    }
1658                }
1659                Ok(Value::Optional(None))
1660            }
1661            (Value::List(items), "for_each") => {
1662                let fn_val = arg_values
1663                    .into_iter()
1664                    .next()
1665                    .ok_or(RuntimeError::ArityMismatch {
1666                        expected: 1,
1667                        got: 0,
1668                    })?;
1669                let fn_id = match fn_val {
1670                    Value::Function(ref fv) => fv.id,
1671                    other => {
1672                        return Err(RuntimeError::TypeError(format!(
1673                            "List.for_each expects a function, got {other}"
1674                        )))
1675                    }
1676                };
1677                let closure = self.fn_registry.get(&fn_id).cloned().ok_or_else(|| {
1678                    RuntimeError::NotCallable {
1679                        value: "unregistered fn".to_string(),
1680                    }
1681                })?;
1682                let items = items.clone();
1683                for item in items {
1684                    self.call_closure(&closure, vec![item]).await?;
1685                }
1686                Ok(Value::Void)
1687            }
1688            (Value::List(items), "flat_map") => {
1689                let fn_val = arg_values
1690                    .into_iter()
1691                    .next()
1692                    .ok_or(RuntimeError::ArityMismatch {
1693                        expected: 1,
1694                        got: 0,
1695                    })?;
1696                let fn_id = match fn_val {
1697                    Value::Function(ref fv) => fv.id,
1698                    other => {
1699                        return Err(RuntimeError::TypeError(format!(
1700                            "List.flat_map expects a function, got {other}"
1701                        )))
1702                    }
1703                };
1704                let closure = self.fn_registry.get(&fn_id).cloned().ok_or_else(|| {
1705                    RuntimeError::NotCallable {
1706                        value: "unregistered fn".to_string(),
1707                    }
1708                })?;
1709                let items = items.clone();
1710                let mut result = Vec::new();
1711                for item in items {
1712                    match self.call_closure(&closure, vec![item]).await? {
1713                        Value::List(inner) => result.extend(inner),
1714                        other => result.push(other),
1715                    }
1716                }
1717                Ok(Value::List(result))
1718            }
1719            (Value::List(items), "sort") => {
1720                let mut v = items.clone();
1721                v.sort();
1722                Ok(Value::List(v))
1723            }
1724            (Value::List(items), "dedup") => {
1725                let mut v = items.clone();
1726                v.dedup();
1727                Ok(Value::List(v))
1728            }
1729            (Value::List(items), "flatten") => {
1730                let mut result = Vec::new();
1731                for item in items {
1732                    match item {
1733                        Value::List(inner) => result.extend(inner.iter().cloned()),
1734                        other => result.push(other.clone()),
1735                    }
1736                }
1737                Ok(Value::List(result))
1738            }
1739            (Value::List(items), "zip") => {
1740                let other = match arg_values.first() {
1741                    Some(Value::List(l)) => l,
1742                    _ => {
1743                        return Err(RuntimeError::TypeError(
1744                            "List.zip expects a List argument".to_string(),
1745                        ))
1746                    }
1747                };
1748                let pairs: Vec<Value> = items
1749                    .iter()
1750                    .zip(other.iter())
1751                    .map(|(a, b)| Value::Tuple(vec![a.clone(), b.clone()]))
1752                    .collect();
1753                Ok(Value::List(pairs))
1754            }
1755            (Value::List(items), "concat") => {
1756                let other = match arg_values.first() {
1757                    Some(Value::List(l)) => l,
1758                    _ => {
1759                        return Err(RuntimeError::TypeError(
1760                            "List.concat expects a List argument".to_string(),
1761                        ))
1762                    }
1763                };
1764                let mut new_list = items.clone();
1765                new_list.extend_from_slice(other);
1766                Ok(Value::List(new_list))
1767            }
1768            (Value::List(items), "slice") => {
1769                if arg_values.len() < 2 {
1770                    return Err(RuntimeError::ArityMismatch {
1771                        expected: 2,
1772                        got: arg_values.len(),
1773                    });
1774                }
1775                let start = match &arg_values[0] {
1776                    Value::Int(i) => *i,
1777                    _ => return Err(RuntimeError::TypeError("List.slice expects Int arguments".to_string())),
1778                };
1779                let end = match &arg_values[1] {
1780                    Value::Int(i) => *i,
1781                    _ => return Err(RuntimeError::TypeError("List.slice expects Int arguments".to_string())),
1782                };
1783                let len = items.len() as i64;
1784                let start = start.max(0).min(len) as usize;
1785                let end = end.max(0).min(len) as usize;
1786                if start >= end {
1787                    Ok(Value::List(vec![]))
1788                } else {
1789                    Ok(Value::List(items[start..end].to_vec()))
1790                }
1791            }
1792            (Value::List(items), "take") => {
1793                let n = match arg_values.first() {
1794                    Some(Value::Int(i)) => *i,
1795                    _ => return Err(RuntimeError::TypeError("List.take expects Int".to_string())),
1796                };
1797                let n = (n.max(0) as usize).min(items.len());
1798                Ok(Value::List(items[..n].to_vec()))
1799            }
1800            (Value::List(items), "skip") => {
1801                let n = match arg_values.first() {
1802                    Some(Value::Int(i)) => *i,
1803                    _ => return Err(RuntimeError::TypeError("List.skip expects Int".to_string())),
1804                };
1805                let n = (n.max(0) as usize).min(items.len());
1806                Ok(Value::List(items[n..].to_vec()))
1807            }
1808            (Value::List(items), "enumerate") => {
1809                let result: Vec<Value> = items
1810                    .iter()
1811                    .enumerate()
1812                    .map(|(i, v)| Value::Tuple(vec![Value::Int(i as i64), v.clone()]))
1813                    .collect();
1814                Ok(Value::List(result))
1815            }
1816            (Value::List(items), "count") => Ok(Value::Int(items.len() as i64)),
1817            (Value::List(items), "pop") => {
1818                if items.is_empty() {
1819                    Ok(Value::List(vec![]))
1820                } else {
1821                    Ok(Value::List(items[..items.len() - 1].to_vec()))
1822                }
1823            }
1824            (Value::List(items), "insert") => {
1825                if arg_values.len() < 2 {
1826                    return Err(RuntimeError::ArityMismatch {
1827                        expected: 2,
1828                        got: arg_values.len(),
1829                    });
1830                }
1831                let idx = match &arg_values[0] {
1832                    Value::Int(i) => *i as usize,
1833                    _ => return Err(RuntimeError::TypeError("List.insert expects Int index".to_string())),
1834                };
1835                if idx > items.len() {
1836                    return Err(RuntimeError::IndexOutOfBounds {
1837                        index: idx as i64,
1838                        len: items.len(),
1839                    });
1840                }
1841                let mut new_list = items.clone();
1842                new_list.insert(idx, arg_values[1].clone());
1843                Ok(Value::List(new_list))
1844            }
1845            (Value::List(items), "remove") => {
1846                let idx = match arg_values.first() {
1847                    Some(Value::Int(i)) => *i,
1848                    _ => return Err(RuntimeError::TypeError("List.remove expects Int index".to_string())),
1849                };
1850                if idx < 0 || idx as usize >= items.len() {
1851                    return Err(RuntimeError::IndexOutOfBounds {
1852                        index: idx,
1853                        len: items.len(),
1854                    });
1855                }
1856                let mut new_list = items.clone();
1857                new_list.remove(idx as usize);
1858                Ok(Value::List(new_list))
1859            }
1860            (Value::List(items), "index_of") => {
1861                let needle = arg_values.first().ok_or(RuntimeError::ArityMismatch {
1862                    expected: 1,
1863                    got: 0,
1864                })?;
1865                match items.iter().position(|v| v == needle) {
1866                    Some(pos) => Ok(Value::Optional(Some(Box::new(Value::Int(pos as i64))))),
1867                    None => Ok(Value::Optional(None)),
1868                }
1869            }
1870            (Value::List(items), "join") => {
1871                let sep = match arg_values.first() {
1872                    Some(Value::String(s)) => s.as_str().to_owned(),
1873                    _ => String::new(),
1874                };
1875                let parts: Vec<String> = items.iter().map(|v| v.to_string()).collect();
1876                Ok(Value::String(BockString::new(parts.join(&sep))))
1877            }
1878            (Value::List(items), "to_set") => {
1879                let set: std::collections::BTreeSet<Value> = items.iter().cloned().collect();
1880                Ok(Value::Set(set))
1881            }
1882
1883            // ── String ────────────────────────────────────────────────────
1884            (Value::String(s), "len") => Ok(Value::Int(s.as_str().chars().count() as i64)),
1885            (Value::String(s), "is_empty") => Ok(Value::Bool(s.as_str().is_empty())),
1886            (Value::String(s), "to_upper") => {
1887                Ok(Value::String(BockString::new(s.as_str().to_uppercase())))
1888            }
1889            (Value::String(s), "to_lower") => {
1890                Ok(Value::String(BockString::new(s.as_str().to_lowercase())))
1891            }
1892            (Value::String(s), "trim") => Ok(Value::String(BockString::new(s.as_str().trim()))),
1893            (Value::String(s), "contains") => {
1894                if let Some(Value::String(sub)) = arg_values.first() {
1895                    Ok(Value::Bool(s.as_str().contains(sub.as_str())))
1896                } else {
1897                    Err(RuntimeError::TypeError(
1898                        "String.contains expects a String".to_string(),
1899                    ))
1900                }
1901            }
1902            (Value::String(s), "starts_with") => {
1903                if let Some(Value::String(prefix)) = arg_values.first() {
1904                    Ok(Value::Bool(s.as_str().starts_with(prefix.as_str())))
1905                } else {
1906                    Err(RuntimeError::TypeError(
1907                        "String.starts_with expects a String".to_string(),
1908                    ))
1909                }
1910            }
1911            (Value::String(s), "ends_with") => {
1912                if let Some(Value::String(suffix)) = arg_values.first() {
1913                    Ok(Value::Bool(s.as_str().ends_with(suffix.as_str())))
1914                } else {
1915                    Err(RuntimeError::TypeError(
1916                        "String.ends_with expects a String".to_string(),
1917                    ))
1918                }
1919            }
1920            (Value::String(s), "split") => {
1921                let sep = match arg_values.first() {
1922                    Some(Value::String(sep)) => sep.as_str().to_owned(),
1923                    _ => {
1924                        return Err(RuntimeError::TypeError(
1925                            "String.split expects a String separator".to_string(),
1926                        ))
1927                    }
1928                };
1929                let parts: Vec<Value> = s
1930                    .as_str()
1931                    .split(&sep)
1932                    .map(|p| Value::String(BockString::new(p)))
1933                    .collect();
1934                Ok(Value::List(parts))
1935            }
1936            (Value::String(s), "replace") => {
1937                if arg_values.len() < 2 {
1938                    return Err(RuntimeError::ArityMismatch {
1939                        expected: 2,
1940                        got: arg_values.len(),
1941                    });
1942                }
1943                let old = match &arg_values[0] {
1944                    Value::String(s) => s.as_str().to_owned(),
1945                    _ => {
1946                        return Err(RuntimeError::TypeError(
1947                            "String.replace expects String arguments".to_string(),
1948                        ))
1949                    }
1950                };
1951                let new_s = match &arg_values[1] {
1952                    Value::String(s) => s.as_str().to_owned(),
1953                    _ => {
1954                        return Err(RuntimeError::TypeError(
1955                            "String.replace expects String arguments".to_string(),
1956                        ))
1957                    }
1958                };
1959                Ok(Value::String(BockString::new(
1960                    s.as_str().replace(&old, &new_s),
1961                )))
1962            }
1963            (Value::String(s), "chars") => {
1964                let chars: Vec<Value> = s.as_str().chars().map(Value::Char).collect();
1965                Ok(Value::List(chars))
1966            }
1967            (Value::String(s), "substring") => {
1968                if arg_values.len() < 2 {
1969                    return Err(RuntimeError::ArityMismatch {
1970                        expected: 2,
1971                        got: arg_values.len(),
1972                    });
1973                }
1974                let start = match &arg_values[0] {
1975                    Value::Int(i) => *i,
1976                    _ => {
1977                        return Err(RuntimeError::TypeError(
1978                            "String.substring expects Int arguments".to_string(),
1979                        ))
1980                    }
1981                };
1982                let end = match &arg_values[1] {
1983                    Value::Int(i) => *i,
1984                    _ => {
1985                        return Err(RuntimeError::TypeError(
1986                            "String.substring expects Int arguments".to_string(),
1987                        ))
1988                    }
1989                };
1990                let chars: Vec<char> = s.as_str().chars().collect();
1991                let len = chars.len() as i64;
1992                let start = start.max(0).min(len) as usize;
1993                let end = end.max(0).min(len) as usize;
1994                if start >= end {
1995                    Ok(Value::String(BockString::new("")))
1996                } else {
1997                    Ok(Value::String(BockString::new(
1998                        chars[start..end].iter().collect::<String>(),
1999                    )))
2000                }
2001            }
2002
2003            // ── Map ───────────────────────────────────────────────────────
2004            (Value::Map(map), "len") => Ok(Value::Int(map.len() as i64)),
2005            (Value::Map(map), "is_empty") => Ok(Value::Bool(map.is_empty())),
2006            (Value::Map(map), "contains_key") => {
2007                let k = arg_values.first().ok_or(RuntimeError::ArityMismatch {
2008                    expected: 1,
2009                    got: 0,
2010                })?;
2011                Ok(Value::Bool(map.contains_key(k)))
2012            }
2013            (Value::Map(map), "get") => {
2014                let k = arg_values.first().ok_or(RuntimeError::ArityMismatch {
2015                    expected: 1,
2016                    got: 0,
2017                })?;
2018                Ok(Value::Optional(map.get(k).cloned().map(Box::new)))
2019            }
2020            (Value::Map(map), "set") => {
2021                if arg_values.len() < 2 {
2022                    return Err(RuntimeError::ArityMismatch {
2023                        expected: 2,
2024                        got: arg_values.len(),
2025                    });
2026                }
2027                let mut new_map = map.clone();
2028                new_map.insert(arg_values.remove(0), arg_values.remove(0));
2029                Ok(Value::Map(new_map))
2030            }
2031            (Value::Map(map), "delete") => {
2032                let k = arg_values.first().ok_or(RuntimeError::ArityMismatch {
2033                    expected: 1,
2034                    got: 0,
2035                })?;
2036                let mut new_map = map.clone();
2037                new_map.remove(k);
2038                Ok(Value::Map(new_map))
2039            }
2040            (Value::Map(map), "merge") => {
2041                let other = match arg_values.first() {
2042                    Some(Value::Map(m)) => m,
2043                    _ => {
2044                        return Err(RuntimeError::TypeError(
2045                            "Map.merge expects a Map argument".to_string(),
2046                        ))
2047                    }
2048                };
2049                let mut new_map = map.clone();
2050                for (k, v) in other {
2051                    new_map.insert(k.clone(), v.clone());
2052                }
2053                Ok(Value::Map(new_map))
2054            }
2055            (Value::Map(map), "keys") => Ok(Value::List(map.keys().cloned().collect())),
2056            (Value::Map(map), "values") => Ok(Value::List(map.values().cloned().collect())),
2057            (Value::Map(map), "entries") => {
2058                let entries: Vec<Value> = map
2059                    .iter()
2060                    .map(|(k, v)| Value::Tuple(vec![k.clone(), v.clone()]))
2061                    .collect();
2062                Ok(Value::List(entries))
2063            }
2064
2065            // ── Set ───────────────────────────────────────────────────────
2066            (Value::Set(set), "len") => Ok(Value::Int(set.len() as i64)),
2067            (Value::Set(set), "is_empty") => Ok(Value::Bool(set.is_empty())),
2068            (Value::Set(set), "contains") => {
2069                let v = arg_values.first().ok_or(RuntimeError::ArityMismatch {
2070                    expected: 1,
2071                    got: 0,
2072                })?;
2073                Ok(Value::Bool(set.contains(v)))
2074            }
2075
2076            // ── Range ─────────────────────────────────────────────────────
2077            (
2078                Value::Range {
2079                    start,
2080                    end,
2081                    inclusive,
2082                    ..
2083                },
2084                "step",
2085            ) => {
2086                let s = start;
2087                let e = end;
2088                let i = inclusive;
2089                if let Some(Value::Int(step)) = arg_values.first() {
2090                    Ok(Value::Range {
2091                        start: *s,
2092                        end: *e,
2093                        inclusive: *i,
2094                        step: *step,
2095                    })
2096                } else {
2097                    Err(RuntimeError::TypeError(
2098                        "Range.step expects an Int".to_string(),
2099                    ))
2100                }
2101            }
2102            (
2103                Value::Range {
2104                    start,
2105                    end,
2106                    inclusive,
2107                    step,
2108                },
2109                "to_list",
2110            ) => Ok(Value::List(range_to_vec(*start, *end, *inclusive, *step))),
2111
2112            // ── Optional / Result ─────────────────────────────────────────
2113            (Value::Optional(Some(inner)), "unwrap") => Ok(*inner.clone()),
2114            (Value::Optional(None), "unwrap") => {
2115                Err(RuntimeError::TypeError("unwrapped None".to_string()))
2116            }
2117            (Value::Optional(opt), "unwrap_or") => {
2118                let default = arg_values
2119                    .into_iter()
2120                    .next()
2121                    .ok_or(RuntimeError::ArityMismatch {
2122                        expected: 1,
2123                        got: 0,
2124                    })?;
2125                Ok(opt.as_deref().cloned().unwrap_or(default))
2126            }
2127            (Value::Result(Ok(inner)), "unwrap") => Ok(*inner.clone()),
2128            (Value::Result(Err(e)), "unwrap") => {
2129                Err(RuntimeError::TypeError(format!("unwrapped Err({e})")))
2130            }
2131
2132            // ── Fallback: check user-defined impl methods, then free functions
2133            (recv_val, _) => {
2134                // Try user-defined impl methods from method_table.
2135                if let Some(result) =
2136                    self.try_call_impl_method(recv_val, &method, arg_values.clone()).await?
2137                {
2138                    return Ok(result);
2139                }
2140                if let Some(fn_val) = self.env.get(&method).cloned() {
2141                    let fn_id = match &fn_val {
2142                        Value::Function(fv) => fv.id,
2143                        _ => {
2144                            return Err(RuntimeError::TypeError(format!(
2145                                "method '{method}' not found on {type_tag}"
2146                            )))
2147                        }
2148                    };
2149                    let closure = self.fn_registry.get(&fn_id).cloned().ok_or_else(|| {
2150                        RuntimeError::NotCallable {
2151                            value: format!("unregistered method '{method}'"),
2152                        }
2153                    })?;
2154                    let mut all_args = vec![recv_val.clone()];
2155                    all_args.extend(arg_values);
2156                    self.call_closure(&closure, all_args).await
2157                } else {
2158                    Err(RuntimeError::TypeError(format!(
2159                        "method '{method}' not found on {type_tag}"
2160                    )))
2161                }
2162            }
2163        }
2164    }
2165
2166    // ── User-defined impl method dispatch ──────────────────────────────────
2167
2168    /// Try to dispatch a method call via the user-defined method table.
2169    ///
2170    /// Returns `Ok(Some(value))` if the method was found and called,
2171    /// `Ok(None)` if no matching method exists, or `Err` on runtime error.
2172    #[async_recursion]
2173    async fn try_call_impl_method(
2174        &mut self,
2175        receiver: &Value,
2176        method: &str,
2177        args: Vec<Value>,
2178    ) -> Result<Option<Value>, RuntimeError> {
2179        let type_name = match receiver {
2180            Value::Record(rv) => &rv.type_name,
2181            _ => return Ok(None),
2182        };
2183
2184        let entry = self
2185            .method_table
2186            .get(type_name)
2187            .and_then(|methods| methods.get(method))
2188            .cloned();
2189
2190        let (param_names, body) = match entry {
2191            Some(e) => e,
2192            None => return Ok(None),
2193        };
2194
2195        // Check if first param is `self` — instance method.
2196        if param_names.first().map(|s| s.as_str()) == Some("self") {
2197            let saved_env = std::mem::replace(&mut self.env, Environment::new());
2198            self.env.push_scope();
2199            self.env.define("self", receiver.clone());
2200            for (name, val) in param_names.iter().skip(1).zip(args) {
2201                self.env.define(name.clone(), val);
2202            }
2203            let result = self.eval_expr(&body).await;
2204            self.env = saved_env;
2205            match result {
2206                Err(RuntimeError::Return(v)) => Ok(Some(*v)),
2207                other => other.map(Some),
2208            }
2209        } else {
2210            // Static method — no self parameter.
2211            let saved_env = std::mem::replace(&mut self.env, Environment::new());
2212            self.env.push_scope();
2213            for (name, val) in param_names.iter().zip(args) {
2214                self.env.define(name.clone(), val);
2215            }
2216            let result = self.eval_expr(&body).await;
2217            self.env = saved_env;
2218            match result {
2219                Err(RuntimeError::Return(v)) => Ok(Some(*v)),
2220                other => other.map(Some),
2221            }
2222        }
2223    }
2224
2225    // ── Field access ───────────────────────────────────────────────────────
2226
2227    #[async_recursion]
2228    async fn eval_field_access(&mut self, object: &AIRNode, field: &str) -> Result<Value, RuntimeError> {
2229        let obj = self.eval_expr(object).await?;
2230        match obj {
2231            Value::Record(rv) => {
2232                rv.fields
2233                    .get(field)
2234                    .cloned()
2235                    .ok_or_else(|| RuntimeError::FieldNotFound {
2236                        field: field.to_string(),
2237                        type_name: rv.type_name.clone(),
2238                    })
2239            }
2240            Value::Enum(ev) => {
2241                if field == "variant" {
2242                    Ok(Value::String(BockString::new(ev.variant.clone())))
2243                } else {
2244                    Err(RuntimeError::FieldNotFound {
2245                        field: field.to_string(),
2246                        type_name: ev.type_name.clone(),
2247                    })
2248                }
2249            }
2250            other => Err(RuntimeError::TypeError(format!(
2251                "cannot access field '{field}' on {other}"
2252            ))),
2253        }
2254    }
2255
2256    // ── Index access ───────────────────────────────────────────────────────
2257
2258    #[async_recursion]
2259    async fn eval_index(&mut self, object: &AIRNode, index: &AIRNode) -> Result<Value, RuntimeError> {
2260        let obj = self.eval_expr(object).await?;
2261        let idx = self.eval_expr(index).await?;
2262        match (obj, idx) {
2263            (Value::List(items), Value::Int(i)) => {
2264                if i < 0 || i as usize >= items.len() {
2265                    Err(RuntimeError::IndexOutOfBounds {
2266                        index: i,
2267                        len: items.len(),
2268                    })
2269                } else {
2270                    Ok(items[i as usize].clone())
2271                }
2272            }
2273            (Value::Tuple(items), Value::Int(i)) => {
2274                if i < 0 || i as usize >= items.len() {
2275                    Err(RuntimeError::IndexOutOfBounds {
2276                        index: i,
2277                        len: items.len(),
2278                    })
2279                } else {
2280                    Ok(items[i as usize].clone())
2281                }
2282            }
2283            (Value::Map(map), key) => map
2284                .get(&key)
2285                .cloned()
2286                .ok_or_else(|| RuntimeError::TypeError(format!("key not found: {key}"))),
2287            (Value::String(s), Value::Int(i)) => {
2288                let chars: Vec<char> = s.as_str().chars().collect();
2289                if i < 0 || i as usize >= chars.len() {
2290                    Err(RuntimeError::IndexOutOfBounds {
2291                        index: i,
2292                        len: chars.len(),
2293                    })
2294                } else {
2295                    Ok(Value::Char(chars[i as usize]))
2296                }
2297            }
2298            (obj, idx) => Err(RuntimeError::TypeError(format!(
2299                "cannot index {obj} with {idx}"
2300            ))),
2301        }
2302    }
2303
2304    // ── Error propagation (`?`) ────────────────────────────────────────────
2305
2306    #[async_recursion]
2307    async fn eval_propagate(&mut self, expr: &AIRNode) -> Result<Value, RuntimeError> {
2308        let val = self.eval_expr(expr).await?;
2309        match val {
2310            Value::Optional(Some(inner)) => Ok(*inner),
2311            Value::Optional(None) => Err(RuntimeError::Propagated(Box::new(Value::Optional(None)))),
2312            Value::Result(Ok(inner)) => Ok(*inner),
2313            Value::Result(Err(e)) => Err(RuntimeError::Propagated(e)),
2314            other => Err(RuntimeError::TypeError(format!(
2315                "? applied to non-Optional/Result: {other}"
2316            ))),
2317        }
2318    }
2319
2320    // ── Record construction ────────────────────────────────────────────────
2321
2322    #[async_recursion]
2323    async fn eval_record_construct(
2324        &mut self,
2325        path: &TypePath,
2326        fields: &[AirRecordField],
2327        spread: Option<&AIRNode>,
2328    ) -> Result<Value, RuntimeError> {
2329        let type_name = path
2330            .segments
2331            .last()
2332            .map(|s| s.name.as_str())
2333            .unwrap_or("")
2334            .to_string();
2335        let mut record_fields: BTreeMap<String, Value> = BTreeMap::new();
2336
2337        // Apply spread first (base record).
2338        if let Some(spread_expr) = spread {
2339            let spread_val = self.eval_expr(spread_expr).await?;
2340            if let Value::Record(rv) = spread_val {
2341                record_fields = rv.fields;
2342            }
2343        }
2344
2345        // Apply explicit fields (override spread values).
2346        for field in fields {
2347            let val = match &field.value {
2348                Some(v) => self.eval_expr(v).await?,
2349                None => {
2350                    // Shorthand: field name resolves as a variable.
2351                    self.env.get(&field.name.name).cloned().ok_or_else(|| {
2352                        RuntimeError::UndefinedVariable {
2353                            name: field.name.name.clone(),
2354                        }
2355                    })?
2356                }
2357            };
2358            record_fields.insert(field.name.name.clone(), val);
2359        }
2360
2361        Ok(Value::Record(RecordValue {
2362            type_name,
2363            fields: record_fields,
2364        }))
2365    }
2366
2367    // ── String interpolation ───────────────────────────────────────────────
2368
2369    #[async_recursion]
2370    async fn eval_interpolation(
2371        &mut self,
2372        parts: &[AirInterpolationPart],
2373    ) -> Result<Value, RuntimeError> {
2374        let mut result = String::new();
2375        for part in parts {
2376            match part {
2377                AirInterpolationPart::Literal(s) => result.push_str(s),
2378                AirInterpolationPart::Expr(expr) => {
2379                    let val = self.eval_expr(expr).await?;
2380                    // Use Displayable.display trait method if registered,
2381                    // otherwise fall back to Rust Display impl.
2382                    let tag = TypeTag::of(&val);
2383                    let displayed =
2384                        match self
2385                            .builtins
2386                            .call(tag, "display", std::slice::from_ref(&val))
2387                        {
2388                            Some(Ok(Value::String(s))) => s.to_string(),
2389                            _ => val.to_string(),
2390                        };
2391                    result.push_str(&displayed);
2392                }
2393            }
2394        }
2395        Ok(Value::String(BockString::new(result)))
2396    }
2397
2398    // ── Range construction ─────────────────────────────────────────────────
2399
2400    #[async_recursion]
2401    async fn eval_range(
2402        &mut self,
2403        lo: &AIRNode,
2404        hi: &AIRNode,
2405        inclusive: bool,
2406    ) -> Result<Value, RuntimeError> {
2407        let lo_val = self.eval_expr(lo).await?;
2408        let hi_val = self.eval_expr(hi).await?;
2409        match (lo_val, hi_val) {
2410            (Value::Int(start), Value::Int(end)) => Ok(Value::Range {
2411                start,
2412                end,
2413                inclusive,
2414                step: 1,
2415            }),
2416            (lo, hi) => Err(RuntimeError::TypeError(format!(
2417                "range bounds must be Int, got {lo} and {hi}"
2418            ))),
2419        }
2420    }
2421
2422    // ── Block evaluation ───────────────────────────────────────────────────
2423
2424    /// Evaluate a block: push scope, execute statements, eval tail expression.
2425    #[async_recursion]
2426    pub async fn eval_block(
2427        &mut self,
2428        stmts: &[AIRNode],
2429        tail: Option<&AIRNode>,
2430    ) -> Result<Value, RuntimeError> {
2431        self.env.push_scope();
2432
2433        for stmt in stmts {
2434            match self.eval_expr(stmt).await {
2435                Ok(_) => {} // discard intermediate statement result
2436                Err(e) => {
2437                    self.env.pop_scope();
2438                    return Err(e);
2439                }
2440            }
2441        }
2442
2443        let result = match tail {
2444            Some(expr) => self.eval_expr(expr).await,
2445            None => Ok(Value::Void),
2446        };
2447
2448        self.env.pop_scope();
2449        result
2450    }
2451
2452    // ── If expression ──────────────────────────────────────────────────────
2453
2454    #[async_recursion]
2455    async fn eval_if(
2456        &mut self,
2457        let_pattern: Option<&AIRNode>,
2458        condition: &AIRNode,
2459        then_block: &AIRNode,
2460        else_block: Option<&AIRNode>,
2461    ) -> Result<Value, RuntimeError> {
2462        if let Some(pat) = let_pattern {
2463            // `if (let pat = expr) { ... }` — condition is the expression to match.
2464            let val = self.eval_expr(condition).await?;
2465            self.env.push_scope();
2466            let matched = self.try_match_pattern(pat, &val).await;
2467            if matched {
2468                let result = self.eval_expr(then_block).await;
2469                self.env.pop_scope();
2470                result
2471            } else {
2472                self.env.pop_scope();
2473                match else_block {
2474                    Some(eb) => self.eval_expr(eb).await,
2475                    None => Ok(Value::Void),
2476                }
2477            }
2478        } else {
2479            let cond = self.eval_expr(condition).await?;
2480            match cond {
2481                Value::Bool(true) => self.eval_expr(then_block).await,
2482                Value::Bool(false) => match else_block {
2483                    Some(eb) => self.eval_expr(eb).await,
2484                    None => Ok(Value::Void),
2485                },
2486                other => Err(RuntimeError::TypeError(format!(
2487                    "if condition must be Bool, got {other}"
2488                ))),
2489            }
2490        }
2491    }
2492
2493    // ── Match expression ───────────────────────────────────────────────────
2494
2495    #[async_recursion]
2496    async fn eval_match(&mut self, scrutinee: &AIRNode, arms: &[AIRNode]) -> Result<Value, RuntimeError> {
2497        let val = self.eval_expr(scrutinee).await?;
2498
2499        // M-075: Check for non-exhaustive match patterns.
2500        // If the scrutinee is an enum and no arm uses a wildcard or catch-all
2501        // bind pattern, emit a warning.
2502        if matches!(&val, Value::Enum(_)) {
2503            let has_catch_all = arms.iter().any(|arm| {
2504                if let NodeKind::MatchArm { pattern, guard, .. } = &arm.kind {
2505                    guard.is_none()
2506                        && matches!(
2507                            pattern.kind,
2508                            NodeKind::WildcardPat | NodeKind::BindPat { .. }
2509                        )
2510                } else {
2511                    false
2512                }
2513            });
2514            if !has_catch_all {
2515                eprintln!(
2516                    "warning: match on enum value may not be exhaustive (no wildcard `_` arm)"
2517                );
2518            }
2519        }
2520
2521        for arm in arms {
2522            if let NodeKind::MatchArm {
2523                pattern,
2524                guard,
2525                body,
2526            } = &arm.kind
2527            {
2528                self.env.push_scope();
2529                let matched = self.try_match_pattern(pattern, &val).await;
2530
2531                let should_exec = if matched {
2532                    if let Some(guard_expr) = guard {
2533                        match self.eval_expr(guard_expr).await {
2534                            Ok(Value::Bool(b)) => b,
2535                            Ok(_) => false,
2536                            Err(_) => false,
2537                        }
2538                    } else {
2539                        true
2540                    }
2541                } else {
2542                    false
2543                };
2544
2545                if should_exec {
2546                    let result = self.eval_expr(body).await;
2547                    self.env.pop_scope();
2548                    return result;
2549                }
2550                self.env.pop_scope();
2551            }
2552        }
2553
2554        Err(RuntimeError::MatchFailed)
2555    }
2556
2557    // ── Pattern matching ───────────────────────────────────────────────────
2558
2559    /// Attempt to match `value` against `pattern`, binding names into the
2560    /// current scope. Returns `true` on a successful match.
2561    #[async_recursion]
2562    pub async fn try_match_pattern(&mut self, pattern: &AIRNode, value: &Value) -> bool {
2563        match &pattern.kind.clone() {
2564            NodeKind::WildcardPat | NodeKind::RestPat => true,
2565
2566            NodeKind::BindPat { name, .. } => {
2567                self.env.define(name.name.clone(), value.clone());
2568                true
2569            }
2570
2571            NodeKind::LiteralPat { lit } => {
2572                matches!(self.eval_literal(lit), Ok(lit_val) if lit_val == *value)
2573            }
2574
2575            NodeKind::ConstructorPat { path, fields } => {
2576                let variant_name = path.segments.last().map(|s| s.name.as_str()).unwrap_or("");
2577                match (variant_name, value) {
2578                    ("Some", Value::Optional(Some(inner))) => {
2579                        fields.len() == 1 && self.try_match_pattern(&fields[0], inner).await
2580                    }
2581                    ("None", Value::Optional(None)) => true,
2582                    ("Ok", Value::Result(Ok(inner))) => {
2583                        fields.is_empty()
2584                            || (fields.len() == 1 && self.try_match_pattern(&fields[0], inner).await)
2585                    }
2586                    ("Err", Value::Result(Err(inner))) => {
2587                        fields.is_empty()
2588                            || (fields.len() == 1 && self.try_match_pattern(&fields[0], inner).await)
2589                    }
2590                    (name, Value::Enum(ev)) if ev.variant == name => {
2591                        match (&ev.payload, fields.len()) {
2592                            (None, 0) => true,
2593                            (Some(inner), 1) => self.try_match_pattern(&fields[0], inner).await,
2594                            _ => false,
2595                        }
2596                    }
2597                    _ => false,
2598                }
2599            }
2600
2601            NodeKind::RecordPat { path, fields, rest } => {
2602                if let Value::Record(rv) = value {
2603                    let type_name = path.segments.last().map(|s| s.name.as_str()).unwrap_or("");
2604                    if rv.type_name != type_name {
2605                        return false;
2606                    }
2607                    if !rest && fields.len() != rv.fields.len() {
2608                        return false;
2609                    }
2610                    for field in fields {
2611                        let field_val = match rv.fields.get(&field.name.name) {
2612                            Some(v) => v.clone(),
2613                            None => return false,
2614                        };
2615                        if let Some(pat) = &field.pattern {
2616                            if !self.try_match_pattern(pat, &field_val).await {
2617                                return false;
2618                            }
2619                        } else {
2620                            self.env.define(field.name.name.clone(), field_val);
2621                        }
2622                    }
2623                    true
2624                } else {
2625                    false
2626                }
2627            }
2628
2629            NodeKind::TuplePat { elems } => {
2630                if let Value::Tuple(vals) = value {
2631                    if elems.len() != vals.len() {
2632                        return false;
2633                    }
2634                    let pairs: Vec<_> = elems
2635                        .iter()
2636                        .zip(vals.iter())
2637                        .map(|(p, v)| (p.clone(), v.clone()))
2638                        .collect();
2639                    for (pat, val) in pairs {
2640                        if !self.try_match_pattern(&pat, &val).await {
2641                            return false;
2642                        }
2643                    }
2644                    true
2645                } else {
2646                    false
2647                }
2648            }
2649
2650            NodeKind::ListPat { elems, rest } => {
2651                if let Value::List(vals) = value {
2652                    if elems.len() > vals.len() {
2653                        return false;
2654                    }
2655                    if rest.is_none() && elems.len() != vals.len() {
2656                        return false;
2657                    }
2658                    let pairs: Vec<_> = elems
2659                        .iter()
2660                        .zip(vals.iter())
2661                        .map(|(p, v)| (p.clone(), v.clone()))
2662                        .collect();
2663                    for (pat, val) in pairs {
2664                        if !self.try_match_pattern(&pat, &val).await {
2665                            return false;
2666                        }
2667                    }
2668                    if let Some(rest_pat) = rest {
2669                        let rest_vals = Value::List(vals[elems.len()..].to_vec());
2670                        let rest_pat = rest_pat.clone();
2671                        self.try_match_pattern(&rest_pat, &rest_vals).await;
2672                    }
2673                    true
2674                } else {
2675                    false
2676                }
2677            }
2678
2679            NodeKind::OrPat { alternatives } => {
2680                let alts: Vec<_> = alternatives.to_vec();
2681                for alt in alts {
2682                    if self.try_match_pattern(&alt, value).await {
2683                        return true;
2684                    }
2685                }
2686                false
2687            }
2688
2689            NodeKind::RangePat { lo, hi, inclusive } => {
2690                let lo = lo.clone();
2691                let hi = hi.clone();
2692                let inclusive = *inclusive;
2693                let lo_val = match self.eval_expr(&lo).await {
2694                    Ok(v) => v,
2695                    Err(_) => return false,
2696                };
2697                let hi_val = match self.eval_expr(&hi).await {
2698                    Ok(v) => v,
2699                    Err(_) => return false,
2700                };
2701                if inclusive {
2702                    *value >= lo_val && *value <= hi_val
2703                } else {
2704                    *value >= lo_val && *value < hi_val
2705                }
2706            }
2707
2708            _ => false,
2709        }
2710    }
2711
2712    /// Bind `value` to `pattern` in the current scope, for use in `let`.
2713    #[async_recursion]
2714    pub async fn bind_pattern(&mut self, pattern: &AIRNode, value: Value) -> Result<(), RuntimeError> {
2715        match &pattern.kind.clone() {
2716            NodeKind::BindPat { name, .. } => {
2717                self.env.define(name.name.clone(), value);
2718                Ok(())
2719            }
2720            NodeKind::WildcardPat => Ok(()),
2721            NodeKind::TuplePat { elems } => {
2722                if let Value::Tuple(vals) = value {
2723                    if elems.len() != vals.len() {
2724                        return Err(RuntimeError::MatchFailed);
2725                    }
2726                    let pairs: Vec<_> = elems
2727                        .iter()
2728                        .zip(vals)
2729                        .map(|(p, v)| (p.clone(), v))
2730                        .collect();
2731                    for (pat, val) in pairs {
2732                        self.bind_pattern(&pat, val).await?;
2733                    }
2734                    Ok(())
2735                } else {
2736                    Err(RuntimeError::MatchFailed)
2737                }
2738            }
2739            _ => {
2740                if self.try_match_pattern(pattern, &value).await {
2741                    Ok(())
2742                } else {
2743                    Err(RuntimeError::MatchFailed)
2744                }
2745            }
2746        }
2747    }
2748
2749    // ── Loop evaluation ────────────────────────────────────────────────────
2750
2751    #[async_recursion]
2752    async fn eval_for(
2753        &mut self,
2754        pattern: &AIRNode,
2755        iterable: &AIRNode,
2756        body: &AIRNode,
2757    ) -> Result<Value, RuntimeError> {
2758        let iter_val = self.eval_expr(iterable).await?;
2759        match iter_val {
2760            Value::List(items) => self.for_loop_items(pattern, body, items).await,
2761            Value::Range {
2762                start,
2763                end,
2764                inclusive,
2765                step,
2766            } => {
2767                let items = range_to_vec(start, end, inclusive, step);
2768                self.for_loop_items(pattern, body, items).await
2769            }
2770            Value::Set(set) => {
2771                let items: Vec<Value> = set.into_iter().collect();
2772                self.for_loop_items(pattern, body, items).await
2773            }
2774            Value::Map(map) => {
2775                let items: Vec<Value> = map
2776                    .into_iter()
2777                    .map(|(k, v)| Value::Tuple(vec![k, v]))
2778                    .collect();
2779                self.for_loop_items(pattern, body, items).await
2780            }
2781            Value::Iterator(it) => self.for_loop_iterator(pattern, body, &it).await,
2782            other => Err(RuntimeError::TypeError(format!(
2783                "cannot iterate over {other}"
2784            ))),
2785        }
2786    }
2787
2788    #[async_recursion]
2789    async fn for_loop_items(
2790        &mut self,
2791        pattern: &AIRNode,
2792        body: &AIRNode,
2793        items: Vec<Value>,
2794    ) -> Result<Value, RuntimeError> {
2795        for item in items {
2796            self.env.push_scope();
2797            let bind_result = self.bind_pattern(pattern, item).await;
2798            if let Err(e) = bind_result {
2799                self.env.pop_scope();
2800                return Err(e);
2801            }
2802            let result = self.eval_expr(body).await;
2803            self.env.pop_scope();
2804            match result {
2805                Ok(_) | Err(RuntimeError::Continue) => {}
2806                Err(RuntimeError::Break(None)) => return Ok(Value::Void),
2807                Err(RuntimeError::Break(Some(v))) => return Ok(*v),
2808                Err(e) => return Err(e),
2809            }
2810        }
2811        Ok(Value::Void)
2812    }
2813
2814    #[async_recursion]
2815    async fn for_loop_iterator(
2816        &mut self,
2817        pattern: &AIRNode,
2818        body: &AIRNode,
2819        it: &crate::value::IteratorValue,
2820    ) -> Result<Value, RuntimeError> {
2821        loop {
2822            let next = {
2823                let mut kind = it.kind.lock().unwrap();
2824                kind.next()
2825            };
2826            match next {
2827                IteratorNext::Some(val) => {
2828                    self.env.push_scope();
2829                    let bind_result = self.bind_pattern(pattern, val).await;
2830                    if let Err(e) = bind_result {
2831                        self.env.pop_scope();
2832                        return Err(e);
2833                    }
2834                    let result = self.eval_expr(body).await;
2835                    self.env.pop_scope();
2836                    match result {
2837                        Ok(_) | Err(RuntimeError::Continue) => {}
2838                        Err(RuntimeError::Break(None)) => return Ok(Value::Void),
2839                        Err(RuntimeError::Break(Some(v))) => return Ok(*v),
2840                        Err(e) => return Err(e),
2841                    }
2842                }
2843                IteratorNext::Done => break,
2844                IteratorNext::NeedsMapCallback { value, func } => {
2845                    let fn_val = Value::Function(func);
2846                    let mapped = self.invoke_callback(&fn_val, &[value]).await?;
2847                    self.env.push_scope();
2848                    let bind_result = self.bind_pattern(pattern, mapped).await;
2849                    if let Err(e) = bind_result {
2850                        self.env.pop_scope();
2851                        return Err(e);
2852                    }
2853                    let result = self.eval_expr(body).await;
2854                    self.env.pop_scope();
2855                    match result {
2856                        Ok(_) | Err(RuntimeError::Continue) => {}
2857                        Err(RuntimeError::Break(None)) => return Ok(Value::Void),
2858                        Err(RuntimeError::Break(Some(v))) => return Ok(*v),
2859                        Err(e) => return Err(e),
2860                    }
2861                }
2862                IteratorNext::NeedsFilterCallback { value, func } => {
2863                    let fn_val = Value::Function(func);
2864                    let keep = self.invoke_callback(&fn_val, std::slice::from_ref(&value)).await?;
2865                    if keep == Value::Bool(true) {
2866                        self.env.push_scope();
2867                        let bind_result = self.bind_pattern(pattern, value).await;
2868                        if let Err(e) = bind_result {
2869                            self.env.pop_scope();
2870                            return Err(e);
2871                        }
2872                        let result = self.eval_expr(body).await;
2873                        self.env.pop_scope();
2874                        match result {
2875                            Ok(_) | Err(RuntimeError::Continue) => {}
2876                            Err(RuntimeError::Break(None)) => return Ok(Value::Void),
2877                            Err(RuntimeError::Break(Some(v))) => return Ok(*v),
2878                            Err(e) => return Err(e),
2879                        }
2880                    }
2881                }
2882            }
2883        }
2884        Ok(Value::Void)
2885    }
2886
2887    #[async_recursion]
2888    async fn eval_while(&mut self, condition: &AIRNode, body: &AIRNode) -> Result<Value, RuntimeError> {
2889        loop {
2890            let cond = self.eval_expr(condition).await?;
2891            match cond {
2892                Value::Bool(false) => break,
2893                Value::Bool(true) => {}
2894                other => {
2895                    return Err(RuntimeError::TypeError(format!(
2896                        "while condition must be Bool, got {other}"
2897                    )))
2898                }
2899            }
2900            match self.eval_expr(body).await {
2901                Ok(_) | Err(RuntimeError::Continue) => {}
2902                Err(RuntimeError::Break(None)) => return Ok(Value::Void),
2903                Err(RuntimeError::Break(Some(v))) => return Ok(*v),
2904                Err(e) => return Err(e),
2905            }
2906        }
2907        Ok(Value::Void)
2908    }
2909
2910    #[async_recursion]
2911    async fn eval_loop(&mut self, body: &AIRNode) -> Result<Value, RuntimeError> {
2912        loop {
2913            match self.eval_expr(body).await {
2914                Ok(_) | Err(RuntimeError::Continue) => {}
2915                Err(RuntimeError::Break(None)) => return Ok(Value::Void),
2916                Err(RuntimeError::Break(Some(v))) => return Ok(*v),
2917                Err(e) => return Err(e),
2918            }
2919        }
2920    }
2921
2922    #[async_recursion]
2923    async fn eval_guard(
2924        &mut self,
2925        let_pattern: Option<&AIRNode>,
2926        condition: &AIRNode,
2927        else_block: &AIRNode,
2928    ) -> Result<Value, RuntimeError> {
2929        if let Some(pat) = let_pattern {
2930            // guard (let pat = expr) — try pattern match, bind into current scope
2931            let val = self.eval_expr(condition).await?;
2932            let matched = self.try_match_pattern(pat, &val).await;
2933            if matched {
2934                Ok(Value::Void)
2935            } else {
2936                self.eval_expr(else_block).await?;
2937                Ok(Value::Void)
2938            }
2939        } else {
2940            let cond = self.eval_expr(condition).await?;
2941            match cond {
2942                Value::Bool(true) => Ok(Value::Void),
2943                Value::Bool(false) => {
2944                    // Execute the divergent else block (return/break/continue/never).
2945                    // We propagate whatever control-flow signal it raises.
2946                    self.eval_expr(else_block).await?;
2947                    Ok(Value::Void)
2948                }
2949                other => Err(RuntimeError::TypeError(format!(
2950                    "guard condition must be Bool, got {other}"
2951                ))),
2952            }
2953        }
2954    }
2955
2956    // ── Statement-level execution ──────────────────────────────────────────
2957
2958    /// Execute a statement node.
2959    ///
2960    /// Returns `None` for pure statements (let bindings, for/while loops,
2961    /// guard) and `Some(value)` for expression-statements (including `loop`
2962    /// which can yield a break value, and blocks).
2963    #[async_recursion]
2964    pub async fn exec_stmt(&mut self, node: &AIRNode) -> Result<Option<Value>, RuntimeError> {
2965        match &node.kind {
2966            NodeKind::LetBinding { .. }
2967            | NodeKind::For { .. }
2968            | NodeKind::While { .. }
2969            | NodeKind::Guard { .. } => {
2970                self.eval_expr(node).await?;
2971                Ok(None)
2972            }
2973            _ => Ok(Some(self.eval_expr(node).await?)),
2974        }
2975    }
2976
2977    /// Execute a block node, returning the value of its tail expression.
2978    ///
2979    /// Handles `NodeKind::Block` directly; falls back to `eval_expr` for any
2980    /// other node kind so callers can pass bare expression nodes too.
2981    #[async_recursion]
2982    pub async fn exec_block(&mut self, block: &AIRNode) -> Result<Value, RuntimeError> {
2983        match &block.kind {
2984            NodeKind::Block { stmts, tail } => self.eval_block(stmts, tail.as_deref()).await,
2985            _ => self.eval_expr(block).await,
2986        }
2987    }
2988}
2989
2990// ─── Helpers ──────────────────────────────────────────────────────────────────
2991
2992/// Materialise a Range into a `Vec<Value::Int>`.
2993fn range_to_vec(start: i64, end: i64, inclusive: bool, step: i64) -> Vec<Value> {
2994    // Determine effective step: if step is the default (1) and the range is
2995    // descending, automatically use -1 so `5..1` produces `[5, 4, 3, 2]`.
2996    let effective_step = if step == 1 && start > end {
2997        -1
2998    } else if step == 0 {
2999        return Vec::new();
3000    } else {
3001        step
3002    };
3003
3004    let mut result = Vec::new();
3005    let mut i = start;
3006    if effective_step > 0 {
3007        while if inclusive { i <= end } else { i < end } {
3008            result.push(Value::Int(i));
3009            i = match i.checked_add(effective_step) {
3010                Some(next) => next,
3011                None => break,
3012            };
3013        }
3014    } else {
3015        // Descending range
3016        while if inclusive { i >= end } else { i > end } {
3017            result.push(Value::Int(i));
3018            i = match i.checked_add(effective_step) {
3019                Some(next) => next,
3020                None => break,
3021            };
3022        }
3023    }
3024    result
3025}
3026
3027// ─── Tests ────────────────────────────────────────────────────────────────────
3028
3029#[cfg(test)]
3030mod tests {
3031    use super::*;
3032    use bock_air::{AirHandlerPair, AirMapEntry, NodeId, NodeIdGen};
3033    use bock_ast::{AssignOp, BinOp, Ident, Literal, TypePath, UnaryOp};
3034    use bock_errors::{FileId, Span};
3035
3036    fn span() -> Span {
3037        Span {
3038            file: FileId(0),
3039            start: 0,
3040            end: 0,
3041        }
3042    }
3043
3044    fn ident(name: &str) -> Ident {
3045        Ident {
3046            name: name.to_string(),
3047            span: span(),
3048        }
3049    }
3050
3051    fn type_path(name: &str) -> TypePath {
3052        TypePath {
3053            segments: vec![ident(name)],
3054            span: span(),
3055        }
3056    }
3057
3058    fn gen() -> NodeIdGen {
3059        NodeIdGen::new()
3060    }
3061
3062    fn node(id: NodeId, kind: NodeKind) -> AIRNode {
3063        AIRNode::new(id, span(), kind)
3064    }
3065
3066    fn int_lit(g: &NodeIdGen, n: i64) -> AIRNode {
3067        node(
3068            g.next(),
3069            NodeKind::Literal {
3070                lit: Literal::Int(n.to_string()),
3071            },
3072        )
3073    }
3074
3075    fn float_lit(g: &NodeIdGen, f: f64) -> AIRNode {
3076        node(
3077            g.next(),
3078            NodeKind::Literal {
3079                lit: Literal::Float(f.to_string()),
3080            },
3081        )
3082    }
3083
3084    fn bool_lit(g: &NodeIdGen, b: bool) -> AIRNode {
3085        node(
3086            g.next(),
3087            NodeKind::Literal {
3088                lit: Literal::Bool(b),
3089            },
3090        )
3091    }
3092
3093    fn str_lit(g: &NodeIdGen, s: &str) -> AIRNode {
3094        node(
3095            g.next(),
3096            NodeKind::Literal {
3097                lit: Literal::String(s.to_string()),
3098            },
3099        )
3100    }
3101
3102    fn var(g: &NodeIdGen, name: &str) -> AIRNode {
3103        node(g.next(), NodeKind::Identifier { name: ident(name) })
3104    }
3105
3106    // ── Literals ──────────────────────────────────────────────────────────
3107
3108    #[tokio::test]
3109    async fn eval_int_literal() {
3110        let mut interp = Interpreter::new();
3111        let g = gen();
3112        assert_eq!(interp.eval_expr(&int_lit(&g, 42)).await, Ok(Value::Int(42)));
3113    }
3114
3115    #[tokio::test]
3116    async fn eval_float_literal() {
3117        let mut interp = Interpreter::new();
3118        let g = gen();
3119        let result = interp.eval_expr(&float_lit(&g, 3.14)).await;
3120        assert!(matches!(result, Ok(Value::Float(_))));
3121    }
3122
3123    #[tokio::test]
3124    async fn eval_bool_literal() {
3125        let mut interp = Interpreter::new();
3126        let g = gen();
3127        assert_eq!(interp.eval_expr(&bool_lit(&g, true)).await, Ok(Value::Bool(true)));
3128        assert_eq!(
3129            interp.eval_expr(&bool_lit(&g, false)).await,
3130            Ok(Value::Bool(false))
3131        );
3132    }
3133
3134    #[tokio::test]
3135    async fn eval_string_literal() {
3136        let mut interp = Interpreter::new();
3137        let g = gen();
3138        assert_eq!(
3139            interp.eval_expr(&str_lit(&g, "hello")).await,
3140            Ok(Value::String(BockString::new("hello")))
3141        );
3142    }
3143
3144    #[tokio::test]
3145    async fn eval_hex_literal() {
3146        let mut interp = Interpreter::new();
3147        let g = gen();
3148        let n = node(
3149            g.next(),
3150            NodeKind::Literal {
3151                lit: Literal::Int("0xFF".to_string()),
3152            },
3153        );
3154        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Int(255)));
3155    }
3156
3157    #[tokio::test]
3158    async fn eval_unit_literal() {
3159        let mut interp = Interpreter::new();
3160        let g = gen();
3161        let n = node(g.next(), NodeKind::Literal { lit: Literal::Unit });
3162        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Void));
3163    }
3164
3165    // ── Arithmetic ────────────────────────────────────────────────────────
3166
3167    #[tokio::test]
3168    async fn eval_add_ints() {
3169        let mut interp = Interpreter::new();
3170        let g = gen();
3171        let n = node(
3172            g.next(),
3173            NodeKind::BinaryOp {
3174                op: BinOp::Add,
3175                left: Box::new(int_lit(&g, 3)),
3176                right: Box::new(int_lit(&g, 4)),
3177            },
3178        );
3179        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Int(7)));
3180    }
3181
3182    #[tokio::test]
3183    async fn eval_add_strings() {
3184        let mut interp = Interpreter::new();
3185        let g = gen();
3186        let n = node(
3187            g.next(),
3188            NodeKind::BinaryOp {
3189                op: BinOp::Add,
3190                left: Box::new(str_lit(&g, "foo")),
3191                right: Box::new(str_lit(&g, "bar")),
3192            },
3193        );
3194        assert_eq!(
3195            interp.eval_expr(&n).await,
3196            Ok(Value::String(BockString::new("foobar")))
3197        );
3198    }
3199
3200    #[tokio::test]
3201    async fn eval_sub_ints() {
3202        let mut interp = Interpreter::new();
3203        let g = gen();
3204        let n = node(
3205            g.next(),
3206            NodeKind::BinaryOp {
3207                op: BinOp::Sub,
3208                left: Box::new(int_lit(&g, 10)),
3209                right: Box::new(int_lit(&g, 3)),
3210            },
3211        );
3212        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Int(7)));
3213    }
3214
3215    #[tokio::test]
3216    async fn eval_mul_ints() {
3217        let mut interp = Interpreter::new();
3218        let g = gen();
3219        let n = node(
3220            g.next(),
3221            NodeKind::BinaryOp {
3222                op: BinOp::Mul,
3223                left: Box::new(int_lit(&g, 6)),
3224                right: Box::new(int_lit(&g, 7)),
3225            },
3226        );
3227        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Int(42)));
3228    }
3229
3230    #[tokio::test]
3231    async fn eval_div_ints() {
3232        let mut interp = Interpreter::new();
3233        let g = gen();
3234        let n = node(
3235            g.next(),
3236            NodeKind::BinaryOp {
3237                op: BinOp::Div,
3238                left: Box::new(int_lit(&g, 10)),
3239                right: Box::new(int_lit(&g, 2)),
3240            },
3241        );
3242        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Int(5)));
3243    }
3244
3245    #[tokio::test]
3246    async fn eval_div_by_zero() {
3247        let mut interp = Interpreter::new();
3248        let g = gen();
3249        let n = node(
3250            g.next(),
3251            NodeKind::BinaryOp {
3252                op: BinOp::Div,
3253                left: Box::new(int_lit(&g, 1)),
3254                right: Box::new(int_lit(&g, 0)),
3255            },
3256        );
3257        assert!(matches!(
3258            interp.eval_expr(&n).await,
3259            Err(RuntimeError::DivisionByZero)
3260        ));
3261    }
3262
3263    #[tokio::test]
3264    async fn eval_pow() {
3265        let mut interp = Interpreter::new();
3266        let g = gen();
3267        let n = node(
3268            g.next(),
3269            NodeKind::BinaryOp {
3270                op: BinOp::Pow,
3271                left: Box::new(int_lit(&g, 2)),
3272                right: Box::new(int_lit(&g, 10)),
3273            },
3274        );
3275        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Int(1024)));
3276    }
3277
3278    // ── Comparison ────────────────────────────────────────────────────────
3279
3280    #[tokio::test]
3281    async fn eval_eq() {
3282        let mut interp = Interpreter::new();
3283        let g = gen();
3284        let n = node(
3285            g.next(),
3286            NodeKind::BinaryOp {
3287                op: BinOp::Eq,
3288                left: Box::new(int_lit(&g, 5)),
3289                right: Box::new(int_lit(&g, 5)),
3290            },
3291        );
3292        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Bool(true)));
3293    }
3294
3295    #[tokio::test]
3296    async fn eval_ne() {
3297        let mut interp = Interpreter::new();
3298        let g = gen();
3299        let n = node(
3300            g.next(),
3301            NodeKind::BinaryOp {
3302                op: BinOp::Ne,
3303                left: Box::new(int_lit(&g, 3)),
3304                right: Box::new(int_lit(&g, 5)),
3305            },
3306        );
3307        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Bool(true)));
3308    }
3309
3310    #[tokio::test]
3311    async fn eval_lt_gt() {
3312        let mut interp = Interpreter::new();
3313        let g = gen();
3314        let lt = node(
3315            g.next(),
3316            NodeKind::BinaryOp {
3317                op: BinOp::Lt,
3318                left: Box::new(int_lit(&g, 1)),
3319                right: Box::new(int_lit(&g, 2)),
3320            },
3321        );
3322        let gt = node(
3323            g.next(),
3324            NodeKind::BinaryOp {
3325                op: BinOp::Gt,
3326                left: Box::new(int_lit(&g, 3)),
3327                right: Box::new(int_lit(&g, 2)),
3328            },
3329        );
3330        assert_eq!(interp.eval_expr(&lt).await, Ok(Value::Bool(true)));
3331        assert_eq!(interp.eval_expr(&gt).await, Ok(Value::Bool(true)));
3332    }
3333
3334    // ── Logical ───────────────────────────────────────────────────────────
3335
3336    #[tokio::test]
3337    async fn eval_and_short_circuit() {
3338        let mut interp = Interpreter::new();
3339        let g = gen();
3340        let n = node(
3341            g.next(),
3342            NodeKind::BinaryOp {
3343                op: BinOp::And,
3344                left: Box::new(bool_lit(&g, false)),
3345                right: Box::new(bool_lit(&g, true)),
3346            },
3347        );
3348        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Bool(false)));
3349    }
3350
3351    #[tokio::test]
3352    async fn eval_or_short_circuit() {
3353        let mut interp = Interpreter::new();
3354        let g = gen();
3355        let n = node(
3356            g.next(),
3357            NodeKind::BinaryOp {
3358                op: BinOp::Or,
3359                left: Box::new(bool_lit(&g, true)),
3360                right: Box::new(bool_lit(&g, false)),
3361            },
3362        );
3363        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Bool(true)));
3364    }
3365
3366    // ── Unary ─────────────────────────────────────────────────────────────
3367
3368    #[tokio::test]
3369    async fn eval_neg() {
3370        let mut interp = Interpreter::new();
3371        let g = gen();
3372        let n = node(
3373            g.next(),
3374            NodeKind::UnaryOp {
3375                op: UnaryOp::Neg,
3376                operand: Box::new(int_lit(&g, 7)),
3377            },
3378        );
3379        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Int(-7)));
3380    }
3381
3382    #[tokio::test]
3383    async fn eval_not() {
3384        let mut interp = Interpreter::new();
3385        let g = gen();
3386        let n = node(
3387            g.next(),
3388            NodeKind::UnaryOp {
3389                op: UnaryOp::Not,
3390                operand: Box::new(bool_lit(&g, false)),
3391            },
3392        );
3393        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Bool(true)));
3394    }
3395
3396    // ── Variable lookup ───────────────────────────────────────────────────
3397
3398    #[tokio::test]
3399    async fn eval_identifier() {
3400        let mut interp = Interpreter::new();
3401        let g = gen();
3402        interp.env.define("x", Value::Int(99));
3403        assert_eq!(interp.eval_expr(&var(&g, "x")).await, Ok(Value::Int(99)));
3404    }
3405
3406    #[tokio::test]
3407    async fn eval_undefined_variable() {
3408        let mut interp = Interpreter::new();
3409        let g = gen();
3410        assert!(matches!(
3411            interp.eval_expr(&var(&g, "y")).await,
3412            Err(RuntimeError::UndefinedVariable { .. })
3413        ));
3414    }
3415
3416    // ── Collection literals ───────────────────────────────────────────────
3417
3418    #[tokio::test]
3419    async fn eval_list_literal() {
3420        let mut interp = Interpreter::new();
3421        let g = gen();
3422        let n = node(
3423            g.next(),
3424            NodeKind::ListLiteral {
3425                elems: vec![int_lit(&g, 1), int_lit(&g, 2), int_lit(&g, 3)],
3426            },
3427        );
3428        assert_eq!(
3429            interp.eval_expr(&n).await,
3430            Ok(Value::List(vec![
3431                Value::Int(1),
3432                Value::Int(2),
3433                Value::Int(3)
3434            ]))
3435        );
3436    }
3437
3438    #[tokio::test]
3439    async fn eval_tuple_literal() {
3440        let mut interp = Interpreter::new();
3441        let g = gen();
3442        let n = node(
3443            g.next(),
3444            NodeKind::TupleLiteral {
3445                elems: vec![int_lit(&g, 1), bool_lit(&g, true)],
3446            },
3447        );
3448        assert_eq!(
3449            interp.eval_expr(&n).await,
3450            Ok(Value::Tuple(vec![Value::Int(1), Value::Bool(true)]))
3451        );
3452    }
3453
3454    #[tokio::test]
3455    async fn eval_map_literal() {
3456        let mut interp = Interpreter::new();
3457        let g = gen();
3458        let n = node(
3459            g.next(),
3460            NodeKind::MapLiteral {
3461                entries: vec![AirMapEntry {
3462                    key: str_lit(&g, "a"),
3463                    value: int_lit(&g, 1),
3464                }],
3465            },
3466        );
3467        let result = interp.eval_expr(&n).await.unwrap();
3468        if let Value::Map(map) = result {
3469            assert_eq!(
3470                map.get(&Value::String(BockString::new("a"))),
3471                Some(&Value::Int(1))
3472            );
3473        } else {
3474            panic!("expected Map");
3475        }
3476    }
3477
3478    #[tokio::test]
3479    async fn eval_set_literal() {
3480        let mut interp = Interpreter::new();
3481        let g = gen();
3482        let n = node(
3483            g.next(),
3484            NodeKind::SetLiteral {
3485                elems: vec![int_lit(&g, 1), int_lit(&g, 2), int_lit(&g, 1)],
3486            },
3487        );
3488        if let Ok(Value::Set(set)) = interp.eval_expr(&n).await {
3489            assert_eq!(set.len(), 2); // duplicates removed
3490        } else {
3491            panic!("expected Set");
3492        }
3493    }
3494
3495    // ── Record construction & field access ────────────────────────────────
3496
3497    #[tokio::test]
3498    async fn eval_record_construct_and_field_access() {
3499        let mut interp = Interpreter::new();
3500        let g = gen();
3501
3502        let record = node(
3503            g.next(),
3504            NodeKind::RecordConstruct {
3505                path: type_path("Point"),
3506                fields: vec![
3507                    AirRecordField {
3508                        name: ident("x"),
3509                        value: Some(Box::new(int_lit(&g, 3))),
3510                    },
3511                    AirRecordField {
3512                        name: ident("y"),
3513                        value: Some(Box::new(int_lit(&g, 4))),
3514                    },
3515                ],
3516                spread: None,
3517            },
3518        );
3519
3520        let rec_val = interp.eval_expr(&record).await.unwrap();
3521        interp.env.define("p", rec_val);
3522
3523        let field_node = node(
3524            g.next(),
3525            NodeKind::FieldAccess {
3526                object: Box::new(var(&g, "p")),
3527                field: ident("x"),
3528            },
3529        );
3530        assert_eq!(interp.eval_expr(&field_node).await, Ok(Value::Int(3)));
3531    }
3532
3533    #[tokio::test]
3534    async fn eval_record_spread() {
3535        let mut interp = Interpreter::new();
3536        let g = gen();
3537
3538        // Build base record {x: 1, y: 2}
3539        let base = node(
3540            g.next(),
3541            NodeKind::RecordConstruct {
3542                path: type_path("Point"),
3543                fields: vec![
3544                    AirRecordField {
3545                        name: ident("x"),
3546                        value: Some(Box::new(int_lit(&g, 1))),
3547                    },
3548                    AirRecordField {
3549                        name: ident("y"),
3550                        value: Some(Box::new(int_lit(&g, 2))),
3551                    },
3552                ],
3553                spread: None,
3554            },
3555        );
3556        let base_val = interp.eval_expr(&base).await.unwrap();
3557        interp.env.define("base", base_val);
3558
3559        // Spread + override y: Point { ..base, y: 99 }
3560        let spread_record = node(
3561            g.next(),
3562            NodeKind::RecordConstruct {
3563                path: type_path("Point"),
3564                fields: vec![AirRecordField {
3565                    name: ident("y"),
3566                    value: Some(Box::new(int_lit(&g, 99))),
3567                }],
3568                spread: Some(Box::new(var(&g, "base"))),
3569            },
3570        );
3571        if let Ok(Value::Record(rv)) = interp.eval_expr(&spread_record).await {
3572            assert_eq!(rv.fields["x"], Value::Int(1));
3573            assert_eq!(rv.fields["y"], Value::Int(99));
3574        } else {
3575            panic!("expected Record");
3576        }
3577    }
3578
3579    // ── Lambda & function call ─────────────────────────────────────────────
3580
3581    #[tokio::test]
3582    async fn eval_lambda_and_call() {
3583        let mut interp = Interpreter::new();
3584        let g = gen();
3585
3586        // (x) => x * 2
3587        let param = node(
3588            g.next(),
3589            NodeKind::Param {
3590                pattern: Box::new(node(
3591                    g.next(),
3592                    NodeKind::BindPat {
3593                        name: ident("x"),
3594                        is_mut: false,
3595                    },
3596                )),
3597                ty: None,
3598                default: None,
3599            },
3600        );
3601        let body = node(
3602            g.next(),
3603            NodeKind::BinaryOp {
3604                op: BinOp::Mul,
3605                left: Box::new(var(&g, "x")),
3606                right: Box::new(int_lit(&g, 2)),
3607            },
3608        );
3609        let lambda = node(
3610            g.next(),
3611            NodeKind::Lambda {
3612                params: vec![param],
3613                body: Box::new(body),
3614            },
3615        );
3616
3617        let fn_val = interp.eval_expr(&lambda).await.unwrap();
3618        interp.env.define("double", fn_val);
3619
3620        let call = node(
3621            g.next(),
3622            NodeKind::Call {
3623                callee: Box::new(var(&g, "double")),
3624                args: vec![AirArg {
3625                    label: None,
3626                    value: int_lit(&g, 5),
3627                }],
3628                type_args: vec![],
3629            },
3630        );
3631        assert_eq!(interp.eval_expr(&call).await, Ok(Value::Int(10)));
3632    }
3633
3634    #[tokio::test]
3635    async fn eval_closure_captures_env() {
3636        let mut interp = Interpreter::new();
3637        let g = gen();
3638
3639        interp.env.define("factor", Value::Int(3));
3640
3641        // (x) => x * factor
3642        let param = node(
3643            g.next(),
3644            NodeKind::Param {
3645                pattern: Box::new(node(
3646                    g.next(),
3647                    NodeKind::BindPat {
3648                        name: ident("x"),
3649                        is_mut: false,
3650                    },
3651                )),
3652                ty: None,
3653                default: None,
3654            },
3655        );
3656        let body = node(
3657            g.next(),
3658            NodeKind::BinaryOp {
3659                op: BinOp::Mul,
3660                left: Box::new(var(&g, "x")),
3661                right: Box::new(var(&g, "factor")),
3662            },
3663        );
3664        let lambda = node(
3665            g.next(),
3666            NodeKind::Lambda {
3667                params: vec![param],
3668                body: Box::new(body),
3669            },
3670        );
3671        let fn_val = interp.eval_expr(&lambda).await.unwrap();
3672        interp.env.define("triple", fn_val);
3673
3674        let call = node(
3675            g.next(),
3676            NodeKind::Call {
3677                callee: Box::new(var(&g, "triple")),
3678                args: vec![AirArg {
3679                    label: None,
3680                    value: int_lit(&g, 4),
3681                }],
3682                type_args: vec![],
3683            },
3684        );
3685        assert_eq!(interp.eval_expr(&call).await, Ok(Value::Int(12)));
3686    }
3687
3688    // ── Pipe operator ─────────────────────────────────────────────────────
3689
3690    #[tokio::test]
3691    async fn eval_pipe_to_function() {
3692        let mut interp = Interpreter::new();
3693        let g = gen();
3694
3695        // Register (x) => x + 1 as "inc"
3696        let param = node(
3697            g.next(),
3698            NodeKind::Param {
3699                pattern: Box::new(node(
3700                    g.next(),
3701                    NodeKind::BindPat {
3702                        name: ident("x"),
3703                        is_mut: false,
3704                    },
3705                )),
3706                ty: None,
3707                default: None,
3708            },
3709        );
3710        let body = node(
3711            g.next(),
3712            NodeKind::BinaryOp {
3713                op: BinOp::Add,
3714                left: Box::new(var(&g, "x")),
3715                right: Box::new(int_lit(&g, 1)),
3716            },
3717        );
3718        let lambda = node(
3719            g.next(),
3720            NodeKind::Lambda {
3721                params: vec![param],
3722                body: Box::new(body),
3723            },
3724        );
3725        let fn_val = interp.eval_expr(&lambda).await.unwrap();
3726        interp.env.define("inc", fn_val);
3727
3728        // 5 |> inc
3729        let pipe = node(
3730            g.next(),
3731            NodeKind::Pipe {
3732                left: Box::new(int_lit(&g, 5)),
3733                right: Box::new(var(&g, "inc")),
3734            },
3735        );
3736        assert_eq!(interp.eval_expr(&pipe).await, Ok(Value::Int(6)));
3737    }
3738
3739    // ── If expression ─────────────────────────────────────────────────────
3740
3741    #[tokio::test]
3742    async fn eval_if_true_branch() {
3743        let mut interp = Interpreter::new();
3744        let g = gen();
3745        let n = node(
3746            g.next(),
3747            NodeKind::If {
3748                let_pattern: None,
3749                condition: Box::new(bool_lit(&g, true)),
3750                then_block: Box::new(int_lit(&g, 1)),
3751                else_block: Some(Box::new(int_lit(&g, 2))),
3752            },
3753        );
3754        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Int(1)));
3755    }
3756
3757    #[tokio::test]
3758    async fn eval_if_false_branch() {
3759        let mut interp = Interpreter::new();
3760        let g = gen();
3761        let n = node(
3762            g.next(),
3763            NodeKind::If {
3764                let_pattern: None,
3765                condition: Box::new(bool_lit(&g, false)),
3766                then_block: Box::new(int_lit(&g, 1)),
3767                else_block: Some(Box::new(int_lit(&g, 2))),
3768            },
3769        );
3770        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Int(2)));
3771    }
3772
3773    // ── Match expression ──────────────────────────────────────────────────
3774
3775    #[tokio::test]
3776    async fn eval_match_literal_pattern() {
3777        let mut interp = Interpreter::new();
3778        let g = gen();
3779
3780        let arm1 = node(
3781            g.next(),
3782            NodeKind::MatchArm {
3783                pattern: Box::new(node(
3784                    g.next(),
3785                    NodeKind::LiteralPat {
3786                        lit: Literal::Int("1".to_string()),
3787                    },
3788                )),
3789                guard: None,
3790                body: Box::new(str_lit(&g, "one")),
3791            },
3792        );
3793        let arm2 = node(
3794            g.next(),
3795            NodeKind::MatchArm {
3796                pattern: Box::new(node(g.next(), NodeKind::WildcardPat)),
3797                guard: None,
3798                body: Box::new(str_lit(&g, "other")),
3799            },
3800        );
3801        let m = node(
3802            g.next(),
3803            NodeKind::Match {
3804                scrutinee: Box::new(int_lit(&g, 1)),
3805                arms: vec![arm1, arm2],
3806            },
3807        );
3808        assert_eq!(
3809            interp.eval_expr(&m).await,
3810            Ok(Value::String(BockString::new("one")))
3811        );
3812    }
3813
3814    #[tokio::test]
3815    async fn eval_match_bind_pattern() {
3816        let mut interp = Interpreter::new();
3817        let g = gen();
3818
3819        let arm = node(
3820            g.next(),
3821            NodeKind::MatchArm {
3822                pattern: Box::new(node(
3823                    g.next(),
3824                    NodeKind::BindPat {
3825                        name: ident("n"),
3826                        is_mut: false,
3827                    },
3828                )),
3829                guard: None,
3830                body: Box::new(node(
3831                    g.next(),
3832                    NodeKind::BinaryOp {
3833                        op: BinOp::Mul,
3834                        left: Box::new(var(&g, "n")),
3835                        right: Box::new(int_lit(&g, 2)),
3836                    },
3837                )),
3838            },
3839        );
3840        let m = node(
3841            g.next(),
3842            NodeKind::Match {
3843                scrutinee: Box::new(int_lit(&g, 5)),
3844                arms: vec![arm],
3845            },
3846        );
3847        assert_eq!(interp.eval_expr(&m).await, Ok(Value::Int(10)));
3848    }
3849
3850    // ── Error propagation ─────────────────────────────────────────────────
3851
3852    #[tokio::test]
3853    async fn eval_propagate_ok() {
3854        let mut interp = Interpreter::new();
3855        let g = gen();
3856        // Ok(42)?  →  42
3857        let ok_node = node(
3858            g.next(),
3859            NodeKind::ResultConstruct {
3860                variant: ResultVariant::Ok,
3861                value: Some(Box::new(int_lit(&g, 42))),
3862            },
3863        );
3864        let prop = node(
3865            g.next(),
3866            NodeKind::Propagate {
3867                expr: Box::new(ok_node),
3868            },
3869        );
3870        assert_eq!(interp.eval_expr(&prop).await, Ok(Value::Int(42)));
3871    }
3872
3873    #[tokio::test]
3874    async fn eval_propagate_err() {
3875        let mut interp = Interpreter::new();
3876        let g = gen();
3877        // Err("boom")?  →  Propagated
3878        let err_node = node(
3879            g.next(),
3880            NodeKind::ResultConstruct {
3881                variant: ResultVariant::Err,
3882                value: Some(Box::new(str_lit(&g, "boom"))),
3883            },
3884        );
3885        let prop = node(
3886            g.next(),
3887            NodeKind::Propagate {
3888                expr: Box::new(err_node),
3889            },
3890        );
3891        assert!(matches!(
3892            interp.eval_expr(&prop).await,
3893            Err(RuntimeError::Propagated(_))
3894        ));
3895    }
3896
3897    #[tokio::test]
3898    async fn eval_propagate_some() {
3899        let mut interp = Interpreter::new();
3900        let g = gen();
3901        // Some(7)? → 7
3902        interp
3903            .env
3904            .define("opt", Value::Optional(Some(Box::new(Value::Int(7)))));
3905        let prop = node(
3906            g.next(),
3907            NodeKind::Propagate {
3908                expr: Box::new(var(&g, "opt")),
3909            },
3910        );
3911        assert_eq!(interp.eval_expr(&prop).await, Ok(Value::Int(7)));
3912    }
3913
3914    #[tokio::test]
3915    async fn eval_propagate_none() {
3916        let mut interp = Interpreter::new();
3917        let g = gen();
3918        interp.env.define("opt", Value::Optional(None));
3919        let prop = node(
3920            g.next(),
3921            NodeKind::Propagate {
3922                expr: Box::new(var(&g, "opt")),
3923            },
3924        );
3925        assert!(matches!(
3926            interp.eval_expr(&prop).await,
3927            Err(RuntimeError::Propagated(_))
3928        ));
3929    }
3930
3931    // ── String interpolation ──────────────────────────────────────────────
3932
3933    #[tokio::test]
3934    async fn eval_interpolation() {
3935        let mut interp = Interpreter::new();
3936        let g = gen();
3937        interp
3938            .env
3939            .define("name", Value::String(BockString::new("world")));
3940        let n = node(
3941            g.next(),
3942            NodeKind::Interpolation {
3943                parts: vec![
3944                    AirInterpolationPart::Literal("Hello, ".to_string()),
3945                    AirInterpolationPart::Expr(Box::new(var(&g, "name"))),
3946                    AirInterpolationPart::Literal("!".to_string()),
3947                ],
3948            },
3949        );
3950        assert_eq!(
3951            interp.eval_expr(&n).await,
3952            Ok(Value::String(BockString::new("Hello, world!")))
3953        );
3954    }
3955
3956    // ── Range ─────────────────────────────────────────────────────────────
3957
3958    #[tokio::test]
3959    async fn eval_range_exclusive() {
3960        let mut interp = Interpreter::new();
3961        let g = gen();
3962        let n = node(
3963            g.next(),
3964            NodeKind::Range {
3965                lo: Box::new(int_lit(&g, 1)),
3966                hi: Box::new(int_lit(&g, 4)),
3967                inclusive: false,
3968            },
3969        );
3970        assert_eq!(
3971            interp.eval_expr(&n).await,
3972            Ok(Value::Range {
3973                start: 1,
3974                end: 4,
3975                inclusive: false,
3976                step: 1
3977            })
3978        );
3979    }
3980
3981    #[tokio::test]
3982    async fn eval_range_inclusive() {
3983        let mut interp = Interpreter::new();
3984        let g = gen();
3985        let n = node(
3986            g.next(),
3987            NodeKind::Range {
3988                lo: Box::new(int_lit(&g, 1)),
3989                hi: Box::new(int_lit(&g, 3)),
3990                inclusive: true,
3991            },
3992        );
3993        assert_eq!(
3994            interp.eval_expr(&n).await,
3995            Ok(Value::Range {
3996                start: 1,
3997                end: 3,
3998                inclusive: true,
3999                step: 1
4000            })
4001        );
4002    }
4003
4004    // ── Block with let binding ─────────────────────────────────────────────
4005
4006    #[tokio::test]
4007    async fn eval_block_with_let_binding() {
4008        let mut interp = Interpreter::new();
4009        let g = gen();
4010
4011        // { let x = 10; x + 5 }
4012        let let_stmt = node(
4013            g.next(),
4014            NodeKind::LetBinding {
4015                is_mut: false,
4016                pattern: Box::new(node(
4017                    g.next(),
4018                    NodeKind::BindPat {
4019                        name: ident("x"),
4020                        is_mut: false,
4021                    },
4022                )),
4023                ty: None,
4024                value: Box::new(int_lit(&g, 10)),
4025            },
4026        );
4027        let tail = node(
4028            g.next(),
4029            NodeKind::BinaryOp {
4030                op: BinOp::Add,
4031                left: Box::new(var(&g, "x")),
4032                right: Box::new(int_lit(&g, 5)),
4033            },
4034        );
4035        let block = node(
4036            g.next(),
4037            NodeKind::Block {
4038                stmts: vec![let_stmt],
4039                tail: Some(Box::new(tail)),
4040            },
4041        );
4042        assert_eq!(interp.eval_expr(&block).await, Ok(Value::Int(15)));
4043    }
4044
4045    // ── Index access ──────────────────────────────────────────────────────
4046
4047    #[tokio::test]
4048    async fn eval_index_list() {
4049        let mut interp = Interpreter::new();
4050        let g = gen();
4051        interp
4052            .env
4053            .define("lst", Value::List(vec![Value::Int(10), Value::Int(20)]));
4054        let n = node(
4055            g.next(),
4056            NodeKind::Index {
4057                object: Box::new(var(&g, "lst")),
4058                index: Box::new(int_lit(&g, 1)),
4059            },
4060        );
4061        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Int(20)));
4062    }
4063
4064    // ── Result construction ───────────────────────────────────────────────
4065
4066    #[tokio::test]
4067    async fn eval_result_ok() {
4068        let mut interp = Interpreter::new();
4069        let g = gen();
4070        let n = node(
4071            g.next(),
4072            NodeKind::ResultConstruct {
4073                variant: ResultVariant::Ok,
4074                value: Some(Box::new(int_lit(&g, 42))),
4075            },
4076        );
4077        assert_eq!(
4078            interp.eval_expr(&n).await,
4079            Ok(Value::Result(Ok(Box::new(Value::Int(42)))))
4080        );
4081    }
4082
4083    #[tokio::test]
4084    async fn eval_result_err() {
4085        let mut interp = Interpreter::new();
4086        let g = gen();
4087        let n = node(
4088            g.next(),
4089            NodeKind::ResultConstruct {
4090                variant: ResultVariant::Err,
4091                value: Some(Box::new(str_lit(&g, "oops"))),
4092            },
4093        );
4094        assert_eq!(
4095            interp.eval_expr(&n).await,
4096            Ok(Value::Result(Err(Box::new(Value::String(
4097                BockString::new("oops")
4098            )))))
4099        );
4100    }
4101
4102    // ── Function composition ──────────────────────────────────────────────
4103
4104    #[tokio::test]
4105    async fn eval_compose_functions() {
4106        let mut interp = Interpreter::new();
4107        let g = gen();
4108
4109        // Register inc = (x) => x + 1
4110        let double_body = node(
4111            g.next(),
4112            NodeKind::BinaryOp {
4113                op: BinOp::Mul,
4114                left: Box::new(var(&g, "x")),
4115                right: Box::new(int_lit(&g, 2)),
4116            },
4117        );
4118        interp.register_fn("double", vec!["x".to_string()], double_body);
4119
4120        let inc_body = node(
4121            g.next(),
4122            NodeKind::BinaryOp {
4123                op: BinOp::Add,
4124                left: Box::new(var(&g, "x")),
4125                right: Box::new(int_lit(&g, 1)),
4126            },
4127        );
4128        interp.register_fn("inc", vec!["x".to_string()], inc_body);
4129
4130        // double >> inc  — apply double first, then inc
4131        let compose = node(
4132            g.next(),
4133            NodeKind::Compose {
4134                left: Box::new(var(&g, "double")),
4135                right: Box::new(var(&g, "inc")),
4136            },
4137        );
4138        let fn_val = interp.eval_expr(&compose).await.unwrap();
4139        interp.env.define("double_then_inc", fn_val);
4140
4141        let call = node(
4142            g.next(),
4143            NodeKind::Call {
4144                callee: Box::new(var(&g, "double_then_inc")),
4145                args: vec![AirArg {
4146                    label: None,
4147                    value: int_lit(&g, 5),
4148                }],
4149                type_args: vec![],
4150            },
4151        );
4152        // double(5) = 10, inc(10) = 11
4153        assert_eq!(interp.eval_expr(&call).await, Ok(Value::Int(11)));
4154    }
4155
4156    // ── Method calls ──────────────────────────────────────────────────────
4157
4158    #[tokio::test]
4159    async fn eval_list_len_method() {
4160        let mut interp = Interpreter::new();
4161        let g = gen();
4162        interp.env.define(
4163            "lst",
4164            Value::List(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
4165        );
4166        let n = node(
4167            g.next(),
4168            NodeKind::MethodCall {
4169                receiver: Box::new(var(&g, "lst")),
4170                method: ident("len"),
4171                type_args: vec![],
4172                args: vec![],
4173            },
4174        );
4175        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Int(3)));
4176    }
4177
4178    #[tokio::test]
4179    async fn eval_list_map_method() {
4180        let mut interp = Interpreter::new();
4181        let g = gen();
4182
4183        // Register double = (x) => x * 2
4184        let body = node(
4185            g.next(),
4186            NodeKind::BinaryOp {
4187                op: BinOp::Mul,
4188                left: Box::new(var(&g, "x")),
4189                right: Box::new(int_lit(&g, 2)),
4190            },
4191        );
4192        interp.register_fn("double", vec!["x".to_string()], body);
4193
4194        interp.env.define(
4195            "lst",
4196            Value::List(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
4197        );
4198
4199        let n = node(
4200            g.next(),
4201            NodeKind::MethodCall {
4202                receiver: Box::new(var(&g, "lst")),
4203                method: ident("map"),
4204                type_args: vec![],
4205                args: vec![AirArg {
4206                    label: None,
4207                    value: var(&g, "double"),
4208                }],
4209            },
4210        );
4211        assert_eq!(
4212            interp.eval_expr(&n).await,
4213            Ok(Value::List(vec![
4214                Value::Int(2),
4215                Value::Int(4),
4216                Value::Int(6)
4217            ]))
4218        );
4219    }
4220
4221    // ── Bitwise ───────────────────────────────────────────────────────────
4222
4223    #[tokio::test]
4224    async fn eval_bitwise_and() {
4225        let mut interp = Interpreter::new();
4226        let g = gen();
4227        let n = node(
4228            g.next(),
4229            NodeKind::BinaryOp {
4230                op: BinOp::BitAnd,
4231                left: Box::new(int_lit(&g, 0b1100)),
4232                right: Box::new(int_lit(&g, 0b1010)),
4233            },
4234        );
4235        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Int(0b1000)));
4236    }
4237
4238    #[tokio::test]
4239    async fn eval_bitwise_or() {
4240        let mut interp = Interpreter::new();
4241        let g = gen();
4242        let n = node(
4243            g.next(),
4244            NodeKind::BinaryOp {
4245                op: BinOp::BitOr,
4246                left: Box::new(int_lit(&g, 0b1100)),
4247                right: Box::new(int_lit(&g, 0b1010)),
4248            },
4249        );
4250        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Int(0b1110)));
4251    }
4252
4253    // ── Statement execution helpers ────────────────────────────────────────
4254
4255    fn bind_pat(g: &NodeIdGen, name: &str) -> AIRNode {
4256        node(
4257            g.next(),
4258            NodeKind::BindPat {
4259                name: ident(name),
4260                is_mut: false,
4261            },
4262        )
4263    }
4264
4265    fn let_stmt(g: &NodeIdGen, pat: AIRNode, val: AIRNode) -> AIRNode {
4266        node(
4267            g.next(),
4268            NodeKind::LetBinding {
4269                is_mut: false,
4270                pattern: Box::new(pat),
4271                ty: None,
4272                value: Box::new(val),
4273            },
4274        )
4275    }
4276
4277    fn assign_node(g: &NodeIdGen, name: &str, val: AIRNode) -> AIRNode {
4278        node(
4279            g.next(),
4280            NodeKind::Assign {
4281                op: AssignOp::Assign,
4282                target: Box::new(var(g, name)),
4283                value: Box::new(val),
4284            },
4285        )
4286    }
4287
4288    fn add(g: &NodeIdGen, left: AIRNode, right: AIRNode) -> AIRNode {
4289        node(
4290            g.next(),
4291            NodeKind::BinaryOp {
4292                op: BinOp::Add,
4293                left: Box::new(left),
4294                right: Box::new(right),
4295            },
4296        )
4297    }
4298
4299    fn lt(g: &NodeIdGen, left: AIRNode, right: AIRNode) -> AIRNode {
4300        node(
4301            g.next(),
4302            NodeKind::BinaryOp {
4303                op: BinOp::Lt,
4304                left: Box::new(left),
4305                right: Box::new(right),
4306            },
4307        )
4308    }
4309
4310    fn block(g: &NodeIdGen, stmts: Vec<AIRNode>, tail: Option<AIRNode>) -> AIRNode {
4311        node(
4312            g.next(),
4313            NodeKind::Block {
4314                stmts,
4315                tail: tail.map(Box::new),
4316            },
4317        )
4318    }
4319
4320    fn list_lit(g: &NodeIdGen, elems: Vec<AIRNode>) -> AIRNode {
4321        node(g.next(), NodeKind::ListLiteral { elems })
4322    }
4323
4324    // ── exec_stmt / exec_block tests ───────────────────────────────────────
4325
4326    #[tokio::test]
4327    async fn exec_stmt_let_binding_returns_none() {
4328        let mut interp = Interpreter::new();
4329        let g = gen();
4330        let stmt = let_stmt(&g, bind_pat(&g, "x"), int_lit(&g, 99));
4331        let result = interp.exec_stmt(&stmt).await.unwrap();
4332        assert_eq!(result, None);
4333        assert_eq!(interp.env.get("x"), Some(&Value::Int(99)));
4334    }
4335
4336    #[tokio::test]
4337    async fn exec_block_returns_tail_expression() {
4338        // { let a = 3; a + 4 }  =>  7
4339        let mut interp = Interpreter::new();
4340        let g = gen();
4341        let blk = block(
4342            &g,
4343            vec![let_stmt(&g, bind_pat(&g, "a"), int_lit(&g, 3))],
4344            Some(add(&g, var(&g, "a"), int_lit(&g, 4))),
4345        );
4346        assert_eq!(interp.exec_block(&blk).await, Ok(Value::Int(7)));
4347    }
4348
4349    #[tokio::test]
4350    async fn block_scope_variables_do_not_leak() {
4351        // { let inner = 99 }  — inner should not be visible afterward
4352        let mut interp = Interpreter::new();
4353        let g = gen();
4354        let blk = block(
4355            &g,
4356            vec![let_stmt(&g, bind_pat(&g, "inner"), int_lit(&g, 99))],
4357            None,
4358        );
4359        interp.exec_block(&blk).await.unwrap();
4360        assert_eq!(interp.env.get("inner"), None);
4361    }
4362
4363    #[tokio::test]
4364    async fn for_loop_iterates_over_list() {
4365        // sum = 0; for x in [1, 2, 3] { sum = sum + x }  => sum == 6
4366        let mut interp = Interpreter::new();
4367        let g = gen();
4368        interp.env.define("sum", Value::Int(0));
4369        let for_node = node(
4370            g.next(),
4371            NodeKind::For {
4372                pattern: Box::new(bind_pat(&g, "x")),
4373                iterable: Box::new(list_lit(
4374                    &g,
4375                    vec![int_lit(&g, 1), int_lit(&g, 2), int_lit(&g, 3)],
4376                )),
4377                body: Box::new(assign_node(
4378                    &g,
4379                    "sum",
4380                    add(&g, var(&g, "sum"), var(&g, "x")),
4381                )),
4382            },
4383        );
4384        assert_eq!(interp.eval_expr(&for_node).await, Ok(Value::Void));
4385        assert_eq!(interp.env.get("sum"), Some(&Value::Int(6)));
4386    }
4387
4388    #[tokio::test]
4389    async fn for_loop_break_exits_early() {
4390        // for x in [1, 2, 3] { break }  — completes without error
4391        let mut interp = Interpreter::new();
4392        let g = gen();
4393        let break_node = node(g.next(), NodeKind::Break { value: None });
4394        let for_node = node(
4395            g.next(),
4396            NodeKind::For {
4397                pattern: Box::new(bind_pat(&g, "x")),
4398                iterable: Box::new(list_lit(
4399                    &g,
4400                    vec![int_lit(&g, 1), int_lit(&g, 2), int_lit(&g, 3)],
4401                )),
4402                body: Box::new(break_node),
4403            },
4404        );
4405        assert_eq!(interp.eval_expr(&for_node).await, Ok(Value::Void));
4406    }
4407
4408    #[tokio::test]
4409    async fn while_loop_does_not_execute_when_false() {
4410        // while (false) { <never reached> }
4411        let mut interp = Interpreter::new();
4412        let g = gen();
4413        let cond = bool_lit(&g, false);
4414        let body = block(&g, vec![], None);
4415        let while_node = node(
4416            g.next(),
4417            NodeKind::While {
4418                condition: Box::new(cond),
4419                body: Box::new(body),
4420            },
4421        );
4422        assert_eq!(interp.eval_expr(&while_node).await, Ok(Value::Void));
4423    }
4424
4425    #[tokio::test]
4426    async fn while_loop_counts_to_three() {
4427        // count = 0; while (count < 3) { count = count + 1 }  => count == 3
4428        let mut interp = Interpreter::new();
4429        let g = gen();
4430        interp.env.define("count", Value::Int(0));
4431        let cond = lt(&g, var(&g, "count"), int_lit(&g, 3));
4432        let body = assign_node(&g, "count", add(&g, var(&g, "count"), int_lit(&g, 1)));
4433        let while_node = node(
4434            g.next(),
4435            NodeKind::While {
4436                condition: Box::new(cond),
4437                body: Box::new(body),
4438            },
4439        );
4440        assert_eq!(interp.eval_expr(&while_node).await, Ok(Value::Void));
4441        assert_eq!(interp.env.get("count"), Some(&Value::Int(3)));
4442    }
4443
4444    #[tokio::test]
4445    async fn loop_break_with_value() {
4446        // loop { break 42 }  => 42
4447        let mut interp = Interpreter::new();
4448        let g = gen();
4449        let break_node = node(
4450            g.next(),
4451            NodeKind::Break {
4452                value: Some(Box::new(int_lit(&g, 42))),
4453            },
4454        );
4455        let loop_node = node(
4456            g.next(),
4457            NodeKind::Loop {
4458                body: Box::new(break_node),
4459            },
4460        );
4461        assert_eq!(interp.eval_expr(&loop_node).await, Ok(Value::Int(42)));
4462    }
4463
4464    #[tokio::test]
4465    async fn loop_break_without_value() {
4466        // loop { break }  => Void
4467        let mut interp = Interpreter::new();
4468        let g = gen();
4469        let break_node = node(g.next(), NodeKind::Break { value: None });
4470        let loop_node = node(
4471            g.next(),
4472            NodeKind::Loop {
4473                body: Box::new(break_node),
4474            },
4475        );
4476        assert_eq!(interp.eval_expr(&loop_node).await, Ok(Value::Void));
4477    }
4478
4479    #[tokio::test]
4480    async fn guard_passes_when_condition_true() {
4481        // guard (true) else { return () }  => Void (else block not executed)
4482        let mut interp = Interpreter::new();
4483        let g = gen();
4484        let else_blk = node(g.next(), NodeKind::Return { value: None });
4485        let guard_node = node(
4486            g.next(),
4487            NodeKind::Guard {
4488                let_pattern: None,
4489                condition: Box::new(bool_lit(&g, true)),
4490                else_block: Box::new(else_blk),
4491            },
4492        );
4493        assert_eq!(interp.eval_expr(&guard_node).await, Ok(Value::Void));
4494    }
4495
4496    #[tokio::test]
4497    async fn guard_else_diverges_when_condition_false() {
4498        // guard (false) else { return () }  => propagates Return signal
4499        let mut interp = Interpreter::new();
4500        let g = gen();
4501        let else_blk = node(g.next(), NodeKind::Return { value: None });
4502        let guard_node = node(
4503            g.next(),
4504            NodeKind::Guard {
4505                let_pattern: None,
4506                condition: Box::new(bool_lit(&g, false)),
4507                else_block: Box::new(else_blk),
4508            },
4509        );
4510        assert_eq!(
4511            interp.eval_expr(&guard_node).await,
4512            Err(RuntimeError::Return(Box::new(Value::Void)))
4513        );
4514    }
4515
4516    #[tokio::test]
4517    async fn let_binding_with_tuple_destructuring() {
4518        // let (a, b) = (1, 2)
4519        let mut interp = Interpreter::new();
4520        let g = gen();
4521        let tuple_pat = node(
4522            g.next(),
4523            NodeKind::TuplePat {
4524                elems: vec![bind_pat(&g, "a"), bind_pat(&g, "b")],
4525            },
4526        );
4527        let tuple_val = node(
4528            g.next(),
4529            NodeKind::TupleLiteral {
4530                elems: vec![int_lit(&g, 1), int_lit(&g, 2)],
4531            },
4532        );
4533        let stmt = let_stmt(&g, tuple_pat, tuple_val);
4534        assert_eq!(interp.exec_stmt(&stmt).await, Ok(None));
4535        assert_eq!(interp.env.get("a"), Some(&Value::Int(1)));
4536        assert_eq!(interp.env.get("b"), Some(&Value::Int(2)));
4537    }
4538
4539    // ── Effect handler runtime tests ─────────────────────────────────────
4540
4541    #[tokio::test]
4542    async fn effect_op_with_no_handler_errors() {
4543        let mut interp = Interpreter::new();
4544        let g = gen();
4545        let effect_op = node(
4546            g.next(),
4547            NodeKind::EffectOp {
4548                effect: type_path("Log"),
4549                operation: ident("log"),
4550                args: vec![],
4551            },
4552        );
4553        let result = interp.eval_expr(&effect_op).await;
4554        assert!(matches!(result, Err(RuntimeError::NoEffectHandler { .. })));
4555        if let Err(RuntimeError::NoEffectHandler { effect }) = result {
4556            assert_eq!(effect, "Log");
4557        }
4558    }
4559
4560    #[tokio::test]
4561    async fn effect_op_dispatches_to_single_fn_handler() {
4562        let mut interp = Interpreter::new();
4563        let g = gen();
4564
4565        // Register a handler function that returns Int(42)
4566        let handler_body = int_lit(&g, 42);
4567        interp.register_fn("my_log", vec!["msg".to_string()], handler_body);
4568
4569        // Set module-level handler
4570        let handler_val = interp.env.get("my_log").cloned().unwrap();
4571        interp
4572            .effect_handlers
4573            .set_module_handler("Log", handler_val);
4574
4575        // Call the effect operation
4576        let effect_op = node(
4577            g.next(),
4578            NodeKind::EffectOp {
4579                effect: type_path("Log"),
4580                operation: ident("log"),
4581                args: vec![AirArg {
4582                    label: None,
4583                    value: str_lit(&g, "hello"),
4584                }],
4585            },
4586        );
4587        let result = interp.eval_expr(&effect_op).await;
4588        assert_eq!(result, Ok(Value::Int(42)));
4589    }
4590
4591    #[tokio::test]
4592    async fn effect_op_dispatches_to_record_handler() {
4593        let mut interp = Interpreter::new();
4594        let g = gen();
4595
4596        // Register the operation function
4597        let op_body = int_lit(&g, 99);
4598        interp.register_fn("_log_op", vec!["msg".to_string()], op_body);
4599        let op_fn = interp.env.get("_log_op").cloned().unwrap();
4600
4601        // Create a record handler with the operation as a field
4602        let mut fields = BTreeMap::new();
4603        fields.insert("log".to_string(), op_fn);
4604        let handler_record = Value::Record(RecordValue {
4605            type_name: "ConsoleLog".to_string(),
4606            fields,
4607        });
4608        interp
4609            .effect_handlers
4610            .set_module_handler("Log", handler_record);
4611
4612        let effect_op = node(
4613            g.next(),
4614            NodeKind::EffectOp {
4615                effect: type_path("Log"),
4616                operation: ident("log"),
4617                args: vec![AirArg {
4618                    label: None,
4619                    value: str_lit(&g, "test"),
4620                }],
4621            },
4622        );
4623        let result = interp.eval_expr(&effect_op).await;
4624        assert_eq!(result, Ok(Value::Int(99)));
4625    }
4626
4627    #[tokio::test]
4628    async fn handling_block_pushes_and_pops_handler() {
4629        let mut interp = Interpreter::new();
4630        let g = gen();
4631
4632        // Register a handler function
4633        let handler_body = int_lit(&g, 7);
4634        interp.register_fn("test_handler", vec!["msg".to_string()], handler_body);
4635
4636        // Create handling block with an effect op call in the body
4637        let effect_op = node(
4638            g.next(),
4639            NodeKind::EffectOp {
4640                effect: type_path("Log"),
4641                operation: ident("log"),
4642                args: vec![AirArg {
4643                    label: None,
4644                    value: str_lit(&g, "inside"),
4645                }],
4646            },
4647        );
4648
4649        let handling = node(
4650            g.next(),
4651            NodeKind::HandlingBlock {
4652                handlers: vec![AirHandlerPair {
4653                    effect: type_path("Log"),
4654                    handler: Box::new(var(&g, "test_handler")),
4655                }],
4656                body: Box::new(effect_op),
4657            },
4658        );
4659
4660        // The handling block should succeed
4661        let result = interp.eval_expr(&handling).await;
4662        assert_eq!(result, Ok(Value::Int(7)));
4663
4664        // After the handling block, the handler should be popped
4665        assert!(interp.effect_handlers.resolve("Log").is_none());
4666    }
4667
4668    #[tokio::test]
4669    async fn handling_block_pops_on_error() {
4670        let mut interp = Interpreter::new();
4671        let g = gen();
4672
4673        // Handling block whose body accesses an undefined variable
4674        let bad_body = var(&g, "nonexistent");
4675        let handler_body = int_lit(&g, 1);
4676        interp.register_fn("h", vec![], handler_body);
4677
4678        let handling = node(
4679            g.next(),
4680            NodeKind::HandlingBlock {
4681                handlers: vec![AirHandlerPair {
4682                    effect: type_path("Log"),
4683                    handler: Box::new(var(&g, "h")),
4684                }],
4685                body: Box::new(bad_body),
4686            },
4687        );
4688
4689        let result = interp.eval_expr(&handling).await;
4690        assert!(result.is_err());
4691        // Handler stack should still be cleaned up
4692        assert!(interp.effect_handlers.resolve("Log").is_none());
4693    }
4694
4695    #[tokio::test]
4696    async fn nested_handling_blocks_innermost_wins() {
4697        let mut interp = Interpreter::new();
4698        let g = gen();
4699
4700        // Outer handler returns 1
4701        let outer_body = int_lit(&g, 1);
4702        interp.register_fn("outer_h", vec!["m".to_string()], outer_body);
4703
4704        // Inner handler returns 2
4705        let inner_body = int_lit(&g, 2);
4706        interp.register_fn("inner_h", vec!["m".to_string()], inner_body);
4707
4708        let effect_op = node(
4709            g.next(),
4710            NodeKind::EffectOp {
4711                effect: type_path("Log"),
4712                operation: ident("log"),
4713                args: vec![AirArg {
4714                    label: None,
4715                    value: str_lit(&g, "test"),
4716                }],
4717            },
4718        );
4719
4720        let inner_handling = node(
4721            g.next(),
4722            NodeKind::HandlingBlock {
4723                handlers: vec![AirHandlerPair {
4724                    effect: type_path("Log"),
4725                    handler: Box::new(var(&g, "inner_h")),
4726                }],
4727                body: Box::new(effect_op),
4728            },
4729        );
4730
4731        let outer_handling = node(
4732            g.next(),
4733            NodeKind::HandlingBlock {
4734                handlers: vec![AirHandlerPair {
4735                    effect: type_path("Log"),
4736                    handler: Box::new(var(&g, "outer_h")),
4737                }],
4738                body: Box::new(inner_handling),
4739            },
4740        );
4741
4742        let result = interp.eval_expr(&outer_handling).await;
4743        assert_eq!(result, Ok(Value::Int(2)));
4744    }
4745
4746    #[tokio::test]
4747    async fn three_layer_resolution_local_over_module_over_project() {
4748        let mut interp = Interpreter::new();
4749        let g = gen();
4750
4751        // Project handler returns 1
4752        let proj_body = int_lit(&g, 1);
4753        interp.register_fn("proj_h", vec!["m".to_string()], proj_body);
4754        let proj_val = interp.env.get("proj_h").cloned().unwrap();
4755        interp.effect_handlers.set_project_handler("Log", proj_val);
4756
4757        // Module handler returns 2
4758        let mod_body = int_lit(&g, 2);
4759        interp.register_fn("mod_h", vec!["m".to_string()], mod_body);
4760        let mod_val = interp.env.get("mod_h").cloned().unwrap();
4761        interp.effect_handlers.set_module_handler("Log", mod_val);
4762
4763        // Local handler returns 3
4764        let local_body = int_lit(&g, 3);
4765        interp.register_fn("local_h", vec!["m".to_string()], local_body);
4766
4767        let effect_op = node(
4768            g.next(),
4769            NodeKind::EffectOp {
4770                effect: type_path("Log"),
4771                operation: ident("log"),
4772                args: vec![AirArg {
4773                    label: None,
4774                    value: str_lit(&g, "test"),
4775                }],
4776            },
4777        );
4778
4779        let handling = node(
4780            g.next(),
4781            NodeKind::HandlingBlock {
4782                handlers: vec![AirHandlerPair {
4783                    effect: type_path("Log"),
4784                    handler: Box::new(var(&g, "local_h")),
4785                }],
4786                body: Box::new(effect_op),
4787            },
4788        );
4789
4790        // Local wins over module and project
4791        let result = interp.eval_expr(&handling).await;
4792        assert_eq!(result, Ok(Value::Int(3)));
4793    }
4794
4795    #[tokio::test]
4796    async fn module_handle_registers_handler() {
4797        let mut interp = Interpreter::new();
4798        let g = gen();
4799
4800        // Register a handler function
4801        let handler_body = int_lit(&g, 55);
4802        interp.register_fn("console_log", vec!["m".to_string()], handler_body);
4803
4804        // Execute ModuleHandle node
4805        let module_handle = node(
4806            g.next(),
4807            NodeKind::ModuleHandle {
4808                effect: type_path("Log"),
4809                handler: Box::new(var(&g, "console_log")),
4810            },
4811        );
4812        let result = interp.eval_expr(&module_handle).await;
4813        assert_eq!(result, Ok(Value::Void));
4814
4815        // Now an effect op should resolve to the module handler
4816        let effect_op = node(
4817            g.next(),
4818            NodeKind::EffectOp {
4819                effect: type_path("Log"),
4820                operation: ident("log"),
4821                args: vec![AirArg {
4822                    label: None,
4823                    value: str_lit(&g, "test"),
4824                }],
4825            },
4826        );
4827        let result = interp.eval_expr(&effect_op).await;
4828        assert_eq!(result, Ok(Value::Int(55)));
4829    }
4830
4831    #[tokio::test]
4832    async fn effect_decl_evaluates_to_void() {
4833        let mut interp = Interpreter::new();
4834        let g = gen();
4835        let effect_decl = node(
4836            g.next(),
4837            NodeKind::EffectDecl {
4838                annotations: vec![],
4839                visibility: bock_ast::Visibility::Private,
4840                name: ident("Log"),
4841                generic_params: vec![],
4842                components: vec![],
4843                operations: vec![],
4844            },
4845        );
4846        let result = interp.eval_expr(&effect_decl).await;
4847        assert_eq!(result, Ok(Value::Void));
4848    }
4849
4850    #[tokio::test]
4851    async fn effect_ref_evaluates_to_void() {
4852        let mut interp = Interpreter::new();
4853        let g = gen();
4854        let effect_ref = node(
4855            g.next(),
4856            NodeKind::EffectRef {
4857                path: type_path("Log"),
4858            },
4859        );
4860        let result = interp.eval_expr(&effect_ref).await;
4861        assert_eq!(result, Ok(Value::Void));
4862    }
4863
4864    #[tokio::test]
4865    async fn no_handler_error_message_is_clear() {
4866        let err = RuntimeError::NoEffectHandler {
4867            effect: "Log".to_string(),
4868        };
4869        let msg = err.to_string();
4870        assert!(msg.contains("Log"));
4871        assert!(msg.contains("handling"));
4872        assert!(msg.contains("handler"));
4873    }
4874
4875    #[tokio::test]
4876    async fn register_effect_dispatches_through_call() {
4877        let mut interp = Interpreter::new();
4878        let g = gen();
4879
4880        // Create an effect with a "log" operation
4881        let empty_body = node(
4882            g.next(),
4883            NodeKind::Block {
4884                stmts: vec![],
4885                tail: None,
4886            },
4887        );
4888        let log_op = node(
4889            g.next(),
4890            NodeKind::FnDecl {
4891                annotations: vec![],
4892                visibility: bock_ast::Visibility::Public,
4893                is_async: false,
4894                name: ident("log"),
4895                generic_params: vec![],
4896                params: vec![],
4897                return_type: None,
4898                effect_clause: vec![],
4899                where_clause: vec![],
4900                body: Box::new(empty_body),
4901            },
4902        );
4903        interp.register_effect("Logger", &[log_op]);
4904
4905        // Register a handler function
4906        let handler_body = int_lit(&g, 77);
4907        interp.register_fn("my_handler", vec!["msg".to_string()], handler_body);
4908        let handler_val = interp.env.get("my_handler").cloned().unwrap();
4909        interp
4910            .effect_handlers
4911            .set_module_handler("Logger", handler_val);
4912
4913        // Call `log("test")` as a regular Call node (how the lowerer emits it)
4914        let call_node = node(
4915            g.next(),
4916            NodeKind::Call {
4917                callee: Box::new(var(&g, "log")),
4918                args: vec![AirArg {
4919                    label: None,
4920                    value: str_lit(&g, "test"),
4921                }],
4922                type_args: vec![],
4923            },
4924        );
4925        let result = interp.eval_expr(&call_node).await;
4926        assert_eq!(result, Ok(Value::Int(77)));
4927    }
4928
4929    // ── M-074: BinOp::Is (runtime type check) ──────────────────────────
4930
4931    #[tokio::test]
4932    async fn is_operator_int() {
4933        let mut interp = Interpreter::new();
4934        let g = gen();
4935        // 42 is Int → true
4936        let n = node(
4937            g.next(),
4938            NodeKind::BinaryOp {
4939                op: BinOp::Is,
4940                left: Box::new(int_lit(&g, 42)),
4941                right: Box::new(str_lit(&g, "Int")),
4942            },
4943        );
4944        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Bool(true)));
4945    }
4946
4947    #[tokio::test]
4948    async fn is_operator_wrong_type() {
4949        let mut interp = Interpreter::new();
4950        let g = gen();
4951        // 42 is String → false
4952        let n = node(
4953            g.next(),
4954            NodeKind::BinaryOp {
4955                op: BinOp::Is,
4956                left: Box::new(int_lit(&g, 42)),
4957                right: Box::new(str_lit(&g, "String")),
4958            },
4959        );
4960        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Bool(false)));
4961    }
4962
4963    #[tokio::test]
4964    async fn is_operator_string() {
4965        let mut interp = Interpreter::new();
4966        let g = gen();
4967        let n = node(
4968            g.next(),
4969            NodeKind::BinaryOp {
4970                op: BinOp::Is,
4971                left: Box::new(str_lit(&g, "hello")),
4972                right: Box::new(str_lit(&g, "String")),
4973            },
4974        );
4975        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Bool(true)));
4976    }
4977
4978    #[tokio::test]
4979    async fn is_operator_bool() {
4980        let mut interp = Interpreter::new();
4981        let g = gen();
4982        let n = node(
4983            g.next(),
4984            NodeKind::BinaryOp {
4985                op: BinOp::Is,
4986                left: Box::new(bool_lit(&g, true)),
4987                right: Box::new(str_lit(&g, "Bool")),
4988            },
4989        );
4990        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Bool(true)));
4991    }
4992
4993    #[tokio::test]
4994    async fn is_operator_list() {
4995        let mut interp = Interpreter::new();
4996        let g = gen();
4997        let list_node = node(
4998            g.next(),
4999            NodeKind::ListLiteral {
5000                elems: vec![int_lit(&g, 1)],
5001            },
5002        );
5003        let n = node(
5004            g.next(),
5005            NodeKind::BinaryOp {
5006                op: BinOp::Is,
5007                left: Box::new(list_node),
5008                right: Box::new(str_lit(&g, "List")),
5009            },
5010        );
5011        assert_eq!(interp.eval_expr(&n).await, Ok(Value::Bool(true)));
5012    }
5013
5014    // ── M-075: Match exhaustiveness (covered by warning output) ─────────
5015
5016    #[tokio::test]
5017    async fn match_on_enum_with_wildcard_succeeds() {
5018        let mut interp = Interpreter::new();
5019        let g = gen();
5020        // Define an enum value
5021        interp.env.define(
5022            "color",
5023            Value::Enum(crate::value::EnumValue {
5024                type_name: "Color".to_string(),
5025                variant: "Red".to_string(),
5026                payload: None,
5027            }),
5028        );
5029        // Match with wildcard arm
5030        let match_expr = node(
5031            g.next(),
5032            NodeKind::Match {
5033                scrutinee: Box::new(var(&g, "color")),
5034                arms: vec![node(
5035                    g.next(),
5036                    NodeKind::MatchArm {
5037                        pattern: Box::new(node(g.next(), NodeKind::WildcardPat)),
5038                        guard: None,
5039                        body: Box::new(int_lit(&g, 99)),
5040                    },
5041                )],
5042            },
5043        );
5044        assert_eq!(interp.eval_expr(&match_expr).await, Ok(Value::Int(99)));
5045    }
5046
5047    // ── M-077: Compound assignment on field/index targets ───────────────
5048
5049    #[tokio::test]
5050    async fn compound_assign_field() {
5051        let mut interp = Interpreter::new();
5052        let g = gen();
5053        // obj = Point { x: 10, y: 20 }
5054        let mut fields = BTreeMap::new();
5055        fields.insert("x".to_string(), Value::Int(10));
5056        fields.insert("y".to_string(), Value::Int(20));
5057        interp.env.define(
5058            "obj",
5059            Value::Record(RecordValue {
5060                type_name: "Point".to_string(),
5061                fields,
5062            }),
5063        );
5064        // obj.x += 5
5065        let assign = node(
5066            g.next(),
5067            NodeKind::Assign {
5068                op: AssignOp::AddAssign,
5069                target: Box::new(node(
5070                    g.next(),
5071                    NodeKind::FieldAccess {
5072                        object: Box::new(var(&g, "obj")),
5073                        field: ident("x"),
5074                    },
5075                )),
5076                value: Box::new(int_lit(&g, 5)),
5077            },
5078        );
5079        assert_eq!(interp.eval_expr(&assign).await, Ok(Value::Void));
5080        // Check that obj.x is now 15
5081        let obj = interp.env.get("obj").unwrap().clone();
5082        if let Value::Record(rv) = obj {
5083            assert_eq!(rv.fields.get("x"), Some(&Value::Int(15)));
5084            assert_eq!(rv.fields.get("y"), Some(&Value::Int(20)));
5085        } else {
5086            panic!("expected Record");
5087        }
5088    }
5089
5090    #[tokio::test]
5091    async fn compound_assign_index() {
5092        let mut interp = Interpreter::new();
5093        let g = gen();
5094        // list = [10, 20, 30]
5095        interp.env.define(
5096            "list",
5097            Value::List(vec![Value::Int(10), Value::Int(20), Value::Int(30)]),
5098        );
5099        // list[1] += 5
5100        let assign = node(
5101            g.next(),
5102            NodeKind::Assign {
5103                op: AssignOp::AddAssign,
5104                target: Box::new(node(
5105                    g.next(),
5106                    NodeKind::Index {
5107                        object: Box::new(var(&g, "list")),
5108                        index: Box::new(int_lit(&g, 1)),
5109                    },
5110                )),
5111                value: Box::new(int_lit(&g, 5)),
5112            },
5113        );
5114        assert_eq!(interp.eval_expr(&assign).await, Ok(Value::Void));
5115        let list = interp.env.get("list").unwrap().clone();
5116        assert_eq!(
5117            list,
5118            Value::List(vec![Value::Int(10), Value::Int(25), Value::Int(30)])
5119        );
5120    }
5121
5122    #[tokio::test]
5123    async fn assign_field_simple() {
5124        let mut interp = Interpreter::new();
5125        let g = gen();
5126        let mut fields = BTreeMap::new();
5127        fields.insert("name".to_string(), Value::String(BockString::new("old")));
5128        interp.env.define(
5129            "obj",
5130            Value::Record(RecordValue {
5131                type_name: "Item".to_string(),
5132                fields,
5133            }),
5134        );
5135        // obj.name = "new"
5136        let assign = node(
5137            g.next(),
5138            NodeKind::Assign {
5139                op: AssignOp::Assign,
5140                target: Box::new(node(
5141                    g.next(),
5142                    NodeKind::FieldAccess {
5143                        object: Box::new(var(&g, "obj")),
5144                        field: ident("name"),
5145                    },
5146                )),
5147                value: Box::new(str_lit(&g, "new")),
5148            },
5149        );
5150        assert_eq!(interp.eval_expr(&assign).await, Ok(Value::Void));
5151        let obj = interp.env.get("obj").unwrap().clone();
5152        if let Value::Record(rv) = obj {
5153            assert_eq!(
5154                rv.fields.get("name"),
5155                Some(&Value::String(BockString::new("new")))
5156            );
5157        } else {
5158            panic!("expected Record");
5159        }
5160    }
5161
5162    // ── M-078: Descending ranges ────────────────────────────────────────
5163
5164    #[tokio::test]
5165    async fn descending_range_exclusive() {
5166        // 5..1 with default step should produce [5, 4, 3, 2]
5167        let result = range_to_vec(5, 1, false, 1);
5168        assert_eq!(
5169            result,
5170            vec![Value::Int(5), Value::Int(4), Value::Int(3), Value::Int(2)]
5171        );
5172    }
5173
5174    #[tokio::test]
5175    async fn descending_range_inclusive() {
5176        // 5..=1 with default step should produce [5, 4, 3, 2, 1]
5177        let result = range_to_vec(5, 1, true, 1);
5178        assert_eq!(
5179            result,
5180            vec![
5181                Value::Int(5),
5182                Value::Int(4),
5183                Value::Int(3),
5184                Value::Int(2),
5185                Value::Int(1),
5186            ]
5187        );
5188    }
5189
5190    #[tokio::test]
5191    async fn descending_range_explicit_negative_step() {
5192        // 10..0 step -2 should produce [10, 8, 6, 4, 2]
5193        let result = range_to_vec(10, 0, false, -2);
5194        assert_eq!(
5195            result,
5196            vec![
5197                Value::Int(10),
5198                Value::Int(8),
5199                Value::Int(6),
5200                Value::Int(4),
5201                Value::Int(2),
5202            ]
5203        );
5204    }
5205
5206    #[tokio::test]
5207    async fn ascending_range_still_works() {
5208        let result = range_to_vec(1, 5, false, 1);
5209        assert_eq!(
5210            result,
5211            vec![Value::Int(1), Value::Int(2), Value::Int(3), Value::Int(4)]
5212        );
5213    }
5214
5215    // ── M-079: for..in over Map ─────────────────────────────────────────
5216
5217    #[tokio::test]
5218    async fn for_in_map() {
5219        let mut interp = Interpreter::new();
5220        let g = gen();
5221        // Build a map {1: "a", 2: "b"}
5222        let mut map = BTreeMap::new();
5223        map.insert(Value::Int(1), Value::String(BockString::new("a")));
5224        map.insert(Value::Int(2), Value::String(BockString::new("b")));
5225        interp.env.define("m", Value::Map(map));
5226        interp.env.define("result", Value::List(vec![]));
5227
5228        // for (k, v) in m { result = result.push(k) }
5229        // Simplified: just iterate and collect keys
5230        let for_expr = node(
5231            g.next(),
5232            NodeKind::For {
5233                pattern: Box::new(node(
5234                    g.next(),
5235                    NodeKind::TuplePat {
5236                        elems: vec![
5237                            node(
5238                                g.next(),
5239                                NodeKind::BindPat {
5240                                    name: ident("k"),
5241                                    is_mut: false,
5242                                },
5243                            ),
5244                            node(
5245                                g.next(),
5246                                NodeKind::BindPat {
5247                                    name: ident("v"),
5248                                    is_mut: false,
5249                                },
5250                            ),
5251                        ],
5252                    },
5253                )),
5254                iterable: Box::new(var(&g, "m")),
5255                body: Box::new(node(
5256                    g.next(),
5257                    NodeKind::Assign {
5258                        op: AssignOp::Assign,
5259                        target: Box::new(var(&g, "result")),
5260                        value: Box::new(node(
5261                            g.next(),
5262                            NodeKind::MethodCall {
5263                                receiver: Box::new(var(&g, "result")),
5264                                method: ident("push"),
5265                                args: vec![AirArg {
5266                                    label: None,
5267                                    value: var(&g, "k"),
5268                                }],
5269                                type_args: vec![],
5270                            },
5271                        )),
5272                    },
5273                )),
5274            },
5275        );
5276        assert_eq!(interp.eval_expr(&for_expr).await, Ok(Value::Void));
5277        let result = interp.env.get("result").unwrap().clone();
5278        // BTreeMap iterates in key order, so keys are [1, 2]
5279        assert_eq!(result, Value::List(vec![Value::Int(1), Value::Int(2)]));
5280    }
5281
5282    // ── M-076: for..in over lazy iterators ──────────────────────────────
5283
5284    #[tokio::test]
5285    async fn for_in_lazy_map_iterator() {
5286        use crate::value::{IteratorKind, IteratorValue};
5287
5288        let mut interp = Interpreter::new();
5289        let g = gen();
5290
5291        // Create a function that doubles its argument
5292        let double_body = node(
5293            g.next(),
5294            NodeKind::BinaryOp {
5295                op: BinOp::Mul,
5296                left: Box::new(var(&g, "x")),
5297                right: Box::new(int_lit(&g, 2)),
5298            },
5299        );
5300        interp.register_fn("double", vec!["x".to_string()], double_body);
5301        let double_fn = match interp.env.get("double").unwrap().clone() {
5302            Value::Function(fv) => fv,
5303            _ => panic!("expected function"),
5304        };
5305
5306        // Create a lazy map iterator over [1, 2, 3] with the double function
5307        let source = IteratorKind::List {
5308            items: vec![Value::Int(1), Value::Int(2), Value::Int(3)],
5309            pos: 0,
5310        };
5311        let map_iter = IteratorKind::Map {
5312            source: std::sync::Arc::new(std::sync::Mutex::new(source)),
5313            func: double_fn,
5314        };
5315        let iter_val = IteratorValue::new(map_iter);
5316        interp.env.define("it", Value::Iterator(iter_val));
5317        interp.env.define("result", Value::List(vec![]));
5318
5319        // for x in it { result = result.push(x) }
5320        let for_expr = node(
5321            g.next(),
5322            NodeKind::For {
5323                pattern: Box::new(node(
5324                    g.next(),
5325                    NodeKind::BindPat {
5326                        name: ident("item"),
5327                        is_mut: false,
5328                    },
5329                )),
5330                iterable: Box::new(var(&g, "it")),
5331                body: Box::new(node(
5332                    g.next(),
5333                    NodeKind::Assign {
5334                        op: AssignOp::Assign,
5335                        target: Box::new(var(&g, "result")),
5336                        value: Box::new(node(
5337                            g.next(),
5338                            NodeKind::MethodCall {
5339                                receiver: Box::new(var(&g, "result")),
5340                                method: ident("push"),
5341                                args: vec![AirArg {
5342                                    label: None,
5343                                    value: var(&g, "item"),
5344                                }],
5345                                type_args: vec![],
5346                            },
5347                        )),
5348                    },
5349                )),
5350            },
5351        );
5352        assert_eq!(interp.eval_expr(&for_expr).await, Ok(Value::Void));
5353        let result = interp.env.get("result").unwrap().clone();
5354        assert_eq!(
5355            result,
5356            Value::List(vec![Value::Int(2), Value::Int(4), Value::Int(6)])
5357        );
5358    }
5359
5360    #[tokio::test]
5361    async fn for_in_lazy_filter_iterator() {
5362        use crate::value::{IteratorKind, IteratorValue};
5363
5364        let mut interp = Interpreter::new();
5365        let g = gen();
5366
5367        // Create a predicate function: x > 2
5368        let pred_body = node(
5369            g.next(),
5370            NodeKind::BinaryOp {
5371                op: BinOp::Gt,
5372                left: Box::new(var(&g, "x")),
5373                right: Box::new(int_lit(&g, 2)),
5374            },
5375        );
5376        interp.register_fn("gt2", vec!["x".to_string()], pred_body);
5377        let gt2_fn = match interp.env.get("gt2").unwrap().clone() {
5378            Value::Function(fv) => fv,
5379            _ => panic!("expected function"),
5380        };
5381
5382        // Create a lazy filter iterator over [1, 2, 3, 4, 5]
5383        let source = IteratorKind::List {
5384            items: vec![
5385                Value::Int(1),
5386                Value::Int(2),
5387                Value::Int(3),
5388                Value::Int(4),
5389                Value::Int(5),
5390            ],
5391            pos: 0,
5392        };
5393        let filter_iter = IteratorKind::Filter {
5394            source: std::sync::Arc::new(std::sync::Mutex::new(source)),
5395            pred: gt2_fn,
5396        };
5397        let iter_val = IteratorValue::new(filter_iter);
5398        interp.env.define("it", Value::Iterator(iter_val));
5399        interp.env.define("result", Value::List(vec![]));
5400
5401        // for item in it { result = result.push(item) }
5402        let for_expr = node(
5403            g.next(),
5404            NodeKind::For {
5405                pattern: Box::new(node(
5406                    g.next(),
5407                    NodeKind::BindPat {
5408                        name: ident("item"),
5409                        is_mut: false,
5410                    },
5411                )),
5412                iterable: Box::new(var(&g, "it")),
5413                body: Box::new(node(
5414                    g.next(),
5415                    NodeKind::Assign {
5416                        op: AssignOp::Assign,
5417                        target: Box::new(var(&g, "result")),
5418                        value: Box::new(node(
5419                            g.next(),
5420                            NodeKind::MethodCall {
5421                                receiver: Box::new(var(&g, "result")),
5422                                method: ident("push"),
5423                                args: vec![AirArg {
5424                                    label: None,
5425                                    value: var(&g, "item"),
5426                                }],
5427                                type_args: vec![],
5428                            },
5429                        )),
5430                    },
5431                )),
5432            },
5433        );
5434        assert_eq!(interp.eval_expr(&for_expr).await, Ok(Value::Void));
5435        let result = interp.env.get("result").unwrap().clone();
5436        assert_eq!(
5437            result,
5438            Value::List(vec![Value::Int(3), Value::Int(4), Value::Int(5)])
5439        );
5440    }
5441}