//! Code generation for the compiler.
//!
//! This crate translates AST nodes into bytecode instructions that can be
//! executed by the virtual machine. The compiler performs a single-pass
//! traversal of the AST, emitting stack-based bytecode operations.
#![forbid(unsafe_code)]

mod registry;
mod symbol;

use std::collections::HashMap;
use std::rc::Rc;

use maat_ast::{FmtSegment, parse_format_string, unescape_string, *};
use maat_bytecode::{
    Bytecode, Instruction, Instructions, MAX_CONSTANT_POOL_SIZE, MAX_ENUM_VARIANTS, Opcode,
    TypeTag, encode,
};
use maat_errors::{CompileError, CompileErrorKind, Error, Result};
use maat_runtime::{CompiledFn, Integer, TypeDef, Value, VariantInfo};
use maat_span::{SourceMap, Span};
use registry::{BUILTIN_METHOD_PREFIXES, VariantEntry};
pub use symbol::{Symbol, SymbolScope, SymbolsTable};

impl CompilationScope {
    fn new() -> Self {
        Self {
            instructions: Instructions::new(),
            last_instruction: None,
            previous_instruction: None,
            source_map: SourceMap::new(),
        }
    }
}

/// Compiler state for generating bytecode from AST nodes.
#[derive(Debug, Clone)]
pub struct Compiler {
    constants: Vec<Value>,
    symbols_table: SymbolsTable,
    scopes: Vec<CompilationScope>,
    scope_index: usize,
    loop_contexts: Vec<LoopContext>,
    for_loop_counter: usize,
    type_registry: Vec<TypeDef>,
    variant_index: HashMap<String, VariantEntry>,
}

/// Per-scope compilation state.
#[derive(Debug, Clone)]
struct CompilationScope {
    instructions: Instructions,
    last_instruction: Option<Instruction>,
    previous_instruction: Option<Instruction>,
    source_map: SourceMap,
}

/// Tracks jump targets for break/continue within a loop.
#[derive(Debug, Clone)]
struct LoopContext {
    label: Option<String>,
    continue_target: Option<usize>,
    break_jumps: Vec<usize>,
    continue_jumps: Vec<usize>,
}

impl Default for Compiler {
    fn default() -> Self {
        Self::new()
    }
}

impl Compiler {
    /// A deterministic dummy target to jump to.
    /// Ultimately replaced by the actual index downstream.
    const JUMP: usize = 9999;

    /// Creates a new compiler with empty instruction stream and constant pool.
    pub fn new() -> Self {
        let mut symbols_table = SymbolsTable::new();
        registry::register_builtins(&mut symbols_table);
        let type_registry = registry::builtin_type_registry();
        let variant_index = registry::build_variant_index(&type_registry);

        Self {
            constants: Vec::new(),
            symbols_table,
            scopes: vec![CompilationScope::new()],
            scope_index: 0,
            loop_contexts: Vec::new(),
            for_loop_counter: 0,
            type_registry,
            variant_index,
        }
    }

    /// Creates a compiler with existing symbols table and constants.
    ///
    /// This enables REPL sessions where variable definitions and constants
    /// persist across multiple compilation passes.
    pub fn with_state(mut symbols_table: SymbolsTable, constants: Vec<Value>) -> Self {
        registry::register_builtins(&mut symbols_table);
        let type_registry = registry::builtin_type_registry();
        let variant_index = registry::build_variant_index(&type_registry);

        Self {
            constants,
            symbols_table,
            scopes: vec![CompilationScope::new()],
            scope_index: 0,
            loop_contexts: Vec::new(),
            for_loop_counter: 0,
            type_registry,
            variant_index,
        }
    }

    /// Returns a mutable reference to the compiler's symbols table.
    ///
    /// Used by the module pipeline to define imported symbols before
    /// compilation, so that cross-module references resolve correctly.
    pub fn symbols_table_mut(&mut self) -> &mut SymbolsTable {
        &mut self.symbols_table
    }

    /// Registers a type definition (struct or enum) in the type registry.
    ///
    /// Used by the module pipeline to register imported type
    /// definitions so that construction and field access work across module
    /// boundaries.
    pub fn register_type(&mut self, typedef: TypeDef) {
        let registry_index = self.type_registry.len();
        if let TypeDef::Enum {
            ref name,
            ref variants,
        } = typedef
        {
            // User-defined enums always get bare variant names in scope.
            registry::index_variants(
                &mut self.variant_index,
                registry_index,
                variants,
                name,
                true,
            );
        }
        self.type_registry.push(typedef);
    }

    /// Extracts the compiled bytecode and constants.
    ///
    /// # Errors
    ///
    /// Returns `CompileError::ScopeUnderflow` if the scope stack is empty,
    /// which indicates an internal compiler invariant violation.
    pub fn bytecode(mut self) -> Result<Bytecode> {
        let scope = self
            .scopes
            .pop()
            .ok_or(CompileError::new(CompileErrorKind::ScopeUnderflow))?;
        Ok(Bytecode {
            instructions: scope.instructions,
            constants: self.constants,
            source_map: scope.source_map,
            type_registry: self.type_registry,
        })
    }

    /// Returns a reference to the compiler's symbols table.
    ///
    /// Used for state persistence in REPL sessions where symbol definitions
    /// must carry over across compilation passes.
    pub fn symbols_table(&self) -> &SymbolsTable {
        &self.symbols_table
    }

    /// Compiles an AST node into bytecode.
    ///
    /// This recursively traverses the AST, emitting instructions for each node.
    /// Returns an error if an unsupported node type or operator is encountered.
    pub fn compile(&mut self, node: &Node) -> Result<()> {
        match node {
            Node::Program(program) => self.compile_program(program),
            Node::Stmt(stmt) => self.compile_statement(stmt),
            Node::Expr(expr) => self.compile_expression(expr),
        }
    }

    /// Compiles a program node (list of statements).
    pub fn compile_program(&mut self, program: &Program) -> Result<()> {
        for stmt in &program.statements {
            if let Stmt::FuncDef(fn_item) = stmt {
                let span = fn_item.span;
                match self.symbols_table.define_symbol(&fn_item.name, false) {
                    Ok(_) => {}
                    Err(e) => return Err(self.attach_span(e, span)),
                }
            }
        }
        for stmt in &program.statements {
            self.compile_statement(stmt)?;
        }
        Ok(())
    }

    /// Compiles a statement node.
    fn compile_statement(&mut self, stmt: &Stmt) -> Result<()> {
        match stmt {
            Stmt::Expr(expr_stmt) => {
                self.compile_expression(&expr_stmt.value)?;
                self.emit(Opcode::Pop, &[], expr_stmt.span);
                Ok(())
            }
            Stmt::Block(block) => self.compile_block_statement(block),
            Stmt::Let(let_stmt) => {
                let span = let_stmt.span;
                if let Some(pattern) = &let_stmt.pattern {
                    self.compile_expression(&let_stmt.value)?;
                    self.compile_let_destructure(pattern, span)?;
                    Ok(())
                } else {
                    if let Expr::Lambda(lambda) = &let_stmt.value {
                        self.compile_fn_body(
                            Some(&let_stmt.ident),
                            lambda.param_names(),
                            lambda.params.len(),
                            &lambda.body,
                            lambda.span,
                        )?;
                    } else {
                        self.compile_expression(&let_stmt.value)?;
                    }
                    self.define_and_set(&let_stmt.ident, let_stmt.mutable, span)?;
                    Ok(())
                }
            }
            Stmt::ReAssign(assign_stmt) => self.compile_reassign(assign_stmt),
            Stmt::Return(ret_stmt) => {
                self.compile_expression(&ret_stmt.value)?;
                self.emit(Opcode::ReturnValue, &[], ret_stmt.span);
                Ok(())
            }
            Stmt::FuncDef(fn_item) => {
                let span = fn_item.span;
                self.compile_fn_body(
                    Some(&fn_item.name),
                    fn_item.param_names(),
                    fn_item.params.len(),
                    &fn_item.body,
                    span,
                )?;
                self.define_and_set(&fn_item.name, false, span)?;
                Ok(())
            }
            Stmt::Loop(loop_stmt) => self.compile_loop(loop_stmt),
            Stmt::While(while_stmt) => self.compile_while(while_stmt),
            Stmt::StructDecl(decl) => {
                self.register_type(TypeDef::Struct {
                    name: decl.name.clone(),
                    field_names: decl.fields.iter().map(|f| f.name.clone()).collect(),
                });
                Ok(())
            }
            Stmt::EnumDecl(decl) => {
                let span = decl.span;
                if decl.variants.len() > MAX_ENUM_VARIANTS {
                    return Err(CompileErrorKind::VariantTagOverflow {
                        name: decl.name.clone(),
                        count: decl.variants.len(),
                        max: MAX_ENUM_VARIANTS,
                    }
                    .at(span)
                    .into());
                }
                let variants = decl
                    .variants
                    .iter()
                    .map(|v| {
                        let count = match &v.kind {
                            EnumVariantKind::Unit => 0,
                            EnumVariantKind::Tuple(fields) => fields.len(),
                            EnumVariantKind::Struct(fields) => fields.len(),
                        };
                        let field_count = u8::try_from(count).map_err(|_| {
                            Error::from(
                                CompileErrorKind::UnsupportedExpr {
                                    expr_type: format!(
                                        "variant `{}` has {count} fields, exceeding the u8 maximum",
                                        v.name
                                    ),
                                }
                                .at(span),
                            )
                        })?;
                        Ok(VariantInfo {
                            name: v.name.clone(),
                            field_count,
                        })
                    })
                    .collect::<Result<Vec<_>>>()?;
                self.register_type(TypeDef::Enum {
                    name: decl.name.clone(),
                    variants,
                });
                Ok(())
            }
            Stmt::TraitDecl(_) => Ok(()),
            Stmt::ImplBlock(impl_block) => self.compile_impl_block(impl_block),
            // Module declarations and import statements are resolved by the
            // module orchestrator before per-module compilation. No-op here.
            Stmt::Use(_) | Stmt::Mod(_) => Ok(()),
            Stmt::For(for_stmt) => {
                if matches!(*for_stmt.iterable, Expr::Range(_)) {
                    self.compile_for_range(for_stmt)?;
                } else if for_stmt.pattern.is_some() {
                    self.compile_for_map(for_stmt)?;
                } else {
                    self.compile_for_array(for_stmt)?;
                }
                Ok(())
            }
        }
    }

    /// Compiles a variable reassignment (`x = expr`).
    fn compile_reassign(&mut self, assign_stmt: &ReAssignStmt) -> Result<()> {
        let span = assign_stmt.span;
        let symbol = self.resolve_or_error(&assign_stmt.ident, span)?;
        if !symbol.mutable {
            return Err(CompileErrorKind::ImmutableAssignment {
                name: assign_stmt.ident.clone(),
            }
            .at(span)
            .into());
        }
        self.compile_expression(&assign_stmt.value)?;
        match symbol.scope {
            SymbolScope::Global => self.emit(Opcode::SetGlobal, &[symbol.index], span),
            SymbolScope::Local | SymbolScope::Free => {
                self.emit(Opcode::SetLocal, &[symbol.index], span)
            }
            SymbolScope::Builtin | SymbolScope::Function => {
                return Err(CompileErrorKind::ImmutableAssignment {
                    name: assign_stmt.ident.clone(),
                }
                .at(span)
                .into());
            }
        };
        Ok(())
    }

    /// Compiles a `#[bounded(N)] loop { body }`.
    ///
    /// Desugars to a counter-guarded loop:
    ///
    /// ```text
    /// let __bound_counter = 0;
    /// loop_start:
    ///     if !(__bound_counter < N) goto loop_exit
    ///     <body>
    /// continue_target:
    ///     __bound_counter += 1
    ///     goto loop_start
    /// loop_exit:
    /// ```
    fn compile_loop(&mut self, loop_stmt: &LoopStmt) -> Result<()> {
        let span = loop_stmt.span;
        let bound = loop_stmt.bound;

        let id = self.for_loop_counter;
        self.for_loop_counter += 1;
        let counter_name = format!("__bound_{id}");
        let bound_name = format!("__blimit_{id}");

        let bound_idx = self.add_constant(Value::Integer(Integer::I64(bound as i64)))?;
        self.emit(Opcode::Constant, &[bound_idx], span);
        let bound_sym = self.define_and_set(&bound_name, false, span)?;

        let zero_idx = self.add_constant(Value::Integer(Integer::I64(0)))?;
        self.emit(Opcode::Constant, &[zero_idx], span);
        let counter_sym = self.define_and_set(&counter_name, false, span)?;

        let loop_start = self.current_instructions().len();
        self.loop_contexts.push(LoopContext {
            label: loop_stmt.label.clone(),
            continue_target: None,
            break_jumps: Vec::new(),
            continue_jumps: Vec::new(),
        });
        self.load_symbol(&counter_sym, span);
        self.load_symbol(&bound_sym, span);
        self.emit(Opcode::LessThan, &[], span);
        let exit_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.compile_block_statement(&loop_stmt.body)?;

        let continue_target = self.current_instructions().len();
        self.load_symbol(&counter_sym, span);
        let one_idx = self.add_constant(Value::Integer(Integer::I64(1)))?;
        self.emit(Opcode::Constant, &[one_idx], span);
        self.emit(Opcode::Add, &[], span);
        self.emit_set_symbol(&counter_sym, span);

        self.emit(Opcode::Jump, &[loop_start], span);
        let loop_exit = self.current_instructions().len();
        self.replace_operand(exit_jump, loop_exit)?;
        let ctx = self
            .loop_contexts
            .pop()
            .expect("loop context was just pushed");
        for jump_pos in ctx.break_jumps {
            self.replace_operand(jump_pos, loop_exit)?;
        }
        for jump_pos in ctx.continue_jumps {
            self.replace_operand(jump_pos, continue_target)?;
        }
        Ok(())
    }

    /// Compiles a `#[bounded(N)] while condition { body }`.
    ///
    /// Desugars to a counter-guarded conditional loop:
    ///
    /// ```text
    /// let __bound_counter = 0;
    /// loop_start:
    ///     if !condition goto loop_exit
    ///     if !(__bound_counter < N) goto loop_exit   // bound exceeded
    ///     <body>
    /// continue_target:
    ///     __bound_counter += 1
    ///     goto loop_start
    /// loop_exit:
    /// ```
    fn compile_while(&mut self, while_stmt: &WhileStmt) -> Result<()> {
        let span = while_stmt.span;
        let bound = while_stmt.bound;

        let id = self.for_loop_counter;
        self.for_loop_counter += 1;
        let counter_name = format!("__bound_{id}");
        let bound_name = format!("__blimit_{id}");

        let bound_idx = self.add_constant(Value::Integer(Integer::I64(bound as i64)))?;
        self.emit(Opcode::Constant, &[bound_idx], span);
        let bound_sym = self.define_and_set(&bound_name, false, span)?;

        let zero_idx = self.add_constant(Value::Integer(Integer::I64(0)))?;
        self.emit(Opcode::Constant, &[zero_idx], span);
        let counter_sym = self.define_and_set(&counter_name, false, span)?;

        let loop_start = self.current_instructions().len();
        self.loop_contexts.push(LoopContext {
            label: while_stmt.label.clone(),
            continue_target: None,
            break_jumps: Vec::new(),
            continue_jumps: Vec::new(),
        });

        self.compile_expression(&while_stmt.condition)?;
        let cond_exit = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.load_symbol(&counter_sym, span);
        self.load_symbol(&bound_sym, span);
        self.emit(Opcode::LessThan, &[], span);
        let bound_exit = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.compile_block_statement(&while_stmt.body)?;

        let continue_target = self.current_instructions().len();
        self.load_symbol(&counter_sym, span);
        let one_idx = self.add_constant(Value::Integer(Integer::I64(1)))?;
        self.emit(Opcode::Constant, &[one_idx], span);
        self.emit(Opcode::Add, &[], span);
        self.emit_set_symbol(&counter_sym, span);

        self.emit(Opcode::Jump, &[loop_start], span);
        let loop_exit = self.current_instructions().len();
        self.replace_operand(cond_exit, loop_exit)?;
        self.replace_operand(bound_exit, loop_exit)?;
        let ctx = self
            .loop_contexts
            .pop()
            .expect("loop context was just pushed");
        for jump_pos in ctx.break_jumps {
            self.replace_operand(jump_pos, loop_exit)?;
        }
        for jump_pos in ctx.continue_jumps {
            self.replace_operand(jump_pos, continue_target)?;
        }
        Ok(())
    }

    /// Compiles a `for x in start..end` or `for x in start..=end` loop.
    ///
    /// Desugars to a counter-based loop without allocating a vector:
    ///
    /// ```text
    /// let __end = <end>;
    /// let __i   = <start>;
    /// loop_start:
    ///     if !(__i < __end) goto loop_exit   // or __i <= __end for inclusive
    ///     let <ident> = __i;
    ///     <body>
    /// continue_target:
    ///     __i = __i + 1
    ///     goto loop_start
    /// loop_exit:
    /// ```
    fn compile_for_range(&mut self, for_stmt: &ForStmt) -> Result<()> {
        let span = for_stmt.span;
        let Expr::Range(ref range) = *for_stmt.iterable else {
            unreachable!("compile_for_range called with non-range iterable");
        };
        let inclusive = range.inclusive;
        let id = self.for_loop_counter;
        self.for_loop_counter += 1;

        let end_name = format!("__end_{id}");
        let i_name = format!("__i_{id}");

        self.compile_expression(&range.end)?;
        let end_sym = self.define_and_set(&end_name, false, span)?;

        self.compile_expression(&range.start)?;
        let i_sym = self.define_and_set(&i_name, false, span)?;

        let loop_start = self.current_instructions().len();
        self.loop_contexts.push(LoopContext {
            label: for_stmt.label.clone(),
            continue_target: None,
            break_jumps: Vec::new(),
            continue_jumps: Vec::new(),
        });
        self.load_symbol(&i_sym, span);
        self.load_symbol(&end_sym, span);
        if inclusive {
            self.emit(Opcode::GreaterThan, &[], span);
            self.emit(Opcode::Bang, &[], span);
        } else {
            self.emit(Opcode::LessThan, &[], span);
        }
        let exit_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.load_symbol(&i_sym, span);
        let _elem_sym = self.define_and_set(&for_stmt.ident, false, span)?;

        self.compile_block_statement(&for_stmt.body)?;

        let elem_kind = range.kind.as_ref();
        self.finalize_counting_loop(&i_sym, loop_start, exit_jump, elem_kind, span)
    }

    /// Compiles a `for x in array_expr` loop via index-based desugaring.
    ///
    /// ```text
    /// let __iter = <iterable>;
    /// let __len  = Vector::len(__iter);
    /// let __i    = 0;
    /// loop_start:
    ///     if !(__i < __len) goto loop_exit
    ///     let <ident> = __iter[__i];
    ///     <body>
    /// continue_target:
    ///     __i = __i + 1
    ///     goto loop_start
    /// loop_exit:
    /// ```
    fn compile_for_array(&mut self, for_stmt: &ForStmt) -> Result<()> {
        let span = for_stmt.span;
        let id = self.for_loop_counter;
        self.for_loop_counter += 1;

        let iter_name = format!("__iter_{id}");
        let len_name = format!("__len_{id}");
        let i_name = format!("__i_{id}");

        self.compile_expression(&for_stmt.iterable)?;
        let iter_sym = self.define_and_set(&iter_name, false, span)?;

        let len_builtin = self.resolve_or_error("Vector::len", span)?;
        self.load_symbol(&len_builtin, span);
        self.load_symbol(&iter_sym, span);
        self.emit(Opcode::Call, &[1], span);
        let len_sym = self.define_and_set(&len_name, false, span)?;

        let zero_idx = self.add_constant(Value::Integer(Integer::I64(0)))?;
        self.emit(Opcode::Constant, &[zero_idx], span);
        let i_sym = self.define_and_set(&i_name, false, span)?;

        let loop_start = self.current_instructions().len();

        self.loop_contexts.push(LoopContext {
            label: for_stmt.label.clone(),
            continue_target: None,
            break_jumps: Vec::new(),
            continue_jumps: Vec::new(),
        });
        self.load_symbol(&i_sym, span);
        self.load_symbol(&len_sym, span);
        self.emit(Opcode::LessThan, &[], span);

        let exit_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.load_symbol(&iter_sym, span);
        self.load_symbol(&i_sym, span);
        self.emit(Opcode::Index, &[], span);
        let _elem_sym = self.define_and_set(&for_stmt.ident, false, span)?;

        // body
        self.compile_block_statement(&for_stmt.body)?;

        self.finalize_counting_loop(&i_sym, loop_start, exit_jump, None, span)
    }

    /// Compiles `for (k, v) in map { body }` by iterating over the map's
    /// insertion-ordered keys and indexing each value.
    ///
    /// Desugars to:
    /// ```text
    /// let __iter = map;
    /// let __keys = Map::keys(__iter);
    /// let __len  = Vector::len(__keys);
    /// let __i    = 0;
    /// loop:
    ///   if __i >= __len: exit
    ///   let __key = __keys[__i];
    ///   let __val = __iter[__key];
    ///   let (k, v) = (__key, __val);  // destructure
    ///   <body>
    ///   __i += 1
    /// ```
    fn compile_for_map(&mut self, for_stmt: &ForStmt) -> Result<()> {
        let span = for_stmt.span;
        let id = self.for_loop_counter;
        self.for_loop_counter += 1;

        let iter_name = format!("__miter_{id}");
        let keys_name = format!("__mkeys_{id}");
        let len_name = format!("__mlen_{id}");
        let i_name = format!("__mi_{id}");

        self.compile_expression(&for_stmt.iterable)?;
        let iter_sym = self.define_and_set(&iter_name, false, span)?;

        let keys_builtin = self.resolve_or_error("Map::keys", span)?;
        self.load_symbol(&keys_builtin, span);
        self.load_symbol(&iter_sym, span);
        self.emit(Opcode::Call, &[1], span);
        let keys_sym = self.define_and_set(&keys_name, false, span)?;

        let len_builtin = self.resolve_or_error("Vector::len", span)?;
        self.load_symbol(&len_builtin, span);
        self.load_symbol(&keys_sym, span);
        self.emit(Opcode::Call, &[1], span);
        let len_sym = self.define_and_set(&len_name, false, span)?;

        let zero_idx = self.add_constant(Value::Integer(Integer::I64(0)))?;
        self.emit(Opcode::Constant, &[zero_idx], span);
        let i_sym = self.define_and_set(&i_name, false, span)?;

        let loop_start = self.current_instructions().len();
        self.loop_contexts.push(LoopContext {
            label: for_stmt.label.clone(),
            continue_target: None,
            break_jumps: Vec::new(),
            continue_jumps: Vec::new(),
        });
        self.load_symbol(&i_sym, span);
        self.load_symbol(&len_sym, span);
        self.emit(Opcode::LessThan, &[], span);
        let exit_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.load_symbol(&keys_sym, span);
        self.load_symbol(&i_sym, span);
        self.emit(Opcode::Index, &[], span);

        let key_name = format!("__mkey_{id}");
        let key_sym = self.define_and_set(&key_name, false, span)?;

        self.load_symbol(&iter_sym, span);
        self.load_symbol(&key_sym, span);
        self.emit(Opcode::Index, &[], span);

        let val_name = format!("__mval_{id}");
        let val_sym = self.define_and_set(&val_name, false, span)?;
        self.load_symbol(&key_sym, span);
        self.load_symbol(&val_sym, span);
        self.emit(Opcode::Tuple, &[2], span);

        let pattern = for_stmt
            .pattern
            .as_ref()
            .expect("compile_for_map requires a pattern");
        self.compile_let_destructure(pattern, span)?;
        self.compile_block_statement(&for_stmt.body)?;
        self.finalize_counting_loop(&i_sym, loop_start, exit_jump, None, span)
    }

    /// Emits the shared tail of a counting for-loop: increment, jump back,
    /// and patch break/continue targets.
    fn finalize_counting_loop(
        &mut self,
        i_sym: &Symbol,
        loop_start: usize,
        exit_jump: usize,
        elem_kind: Option<&NumKind>,
        span: Span,
    ) -> Result<()> {
        let continue_target = self.current_instructions().len();
        self.load_symbol(i_sym, span);
        let one = elem_kind
            .map(Integer::one_of_kind)
            .unwrap_or(Integer::I64(1));
        let one_idx = self.add_constant(Value::Integer(one))?;
        self.emit(Opcode::Constant, &[one_idx], span);
        self.emit(Opcode::Add, &[], span);
        self.emit_set_symbol(i_sym, span);

        self.emit(Opcode::Jump, &[loop_start], span);
        let loop_exit = self.current_instructions().len();
        self.replace_operand(exit_jump, loop_exit)?;
        let ctx = self
            .loop_contexts
            .pop()
            .expect("loop context was just pushed");
        for jump_pos in ctx.break_jumps {
            self.replace_operand(jump_pos, loop_exit)?;
        }
        for jump_pos in ctx.continue_jumps {
            self.replace_operand(jump_pos, continue_target)?;
        }
        Ok(())
    }

    /// Compiles a sequence of statements within a lexical block scope.
    fn compile_block_statement(&mut self, block: &BlockStmt) -> Result<()> {
        self.symbols_table.push_block_scope();
        for stmt in &block.statements {
            self.compile_statement(stmt)?;
        }
        self.symbols_table.pop_block_scope();
        Ok(())
    }

    /// Emits a constant-load instruction for a numeric literal.
    fn compile_numeric_constant(&mut self, val: Value, span: Span) -> Result<()> {
        let index = self.add_constant(val)?;
        self.emit(Opcode::Constant, &[index], span);
        Ok(())
    }

    /// Compiles an expression node into bytecode.
    fn compile_expression(&mut self, expr: &Expr) -> Result<()> {
        let span = expr.span();
        match expr {
            Expr::Number(lit) => {
                let val = Value::from_number_literal(lit)
                    .map_err(|msg| CompileErrorKind::UnsupportedExpr { expr_type: msg }.at(span))?;
                self.compile_numeric_constant(val, span)
            }
            Expr::Bool(b) => {
                let opcode = if b.value { Opcode::True } else { Opcode::False };
                self.emit(opcode, &[], span);
                Ok(())
            }
            Expr::Prefix(prefix_expr) => {
                self.compile_expression(&prefix_expr.operand)?;
                let opcode = match prefix_expr.operator.as_str() {
                    "!" => Opcode::Bang,
                    "-" => Opcode::Minus,
                    op => {
                        return Err(CompileErrorKind::UnsupportedOperator {
                            operator: op.to_string(),
                            context: "prefix expression".to_string(),
                        }
                        .at(span)
                        .into());
                    }
                };
                self.emit(opcode, &[], span);
                Ok(())
            }
            Expr::Infix(infix_expr) => self.compile_infix(infix_expr, span),
            Expr::Cond(cond) => self.compile_conditional(cond),
            Expr::Ident(ident) => {
                if let Some(symbol) = self.symbols_table.resolve_symbol(&ident.value) {
                    self.load_symbol(&symbol, span);
                    Ok(())
                } else if let Some((registry_index, variant_tag, field_count)) =
                    self.find_variant_in_registry(&ident.value)
                {
                    self.emit_variant_constructor(registry_index, variant_tag, field_count, span)
                } else {
                    Err(CompileErrorKind::UndefinedVariable {
                        name: ident.value.clone(),
                    }
                    .at(ident.span)
                    .into())
                }
            }
            Expr::Char(c) => {
                let index = self.add_constant(Value::Char(c.value))?;
                self.emit(Opcode::Constant, &[index], span);
                Ok(())
            }
            Expr::Tuple(tuple) => {
                for element in &tuple.elements {
                    self.compile_expression(element)?;
                }
                self.emit(Opcode::Tuple, &[tuple.elements.len()], span);
                Ok(())
            }
            Expr::Str(s) => {
                let constant = Value::Str(unescape_string(&s.value));
                let index = self.add_constant(constant)?;
                self.emit(Opcode::Constant, &[index], span);
                Ok(())
            }
            Expr::Vector(array) => {
                for element in &array.elements {
                    self.compile_expression(element)?;
                }
                self.emit(Opcode::Vector, &[array.elements.len()], span);
                Ok(())
            }
            Expr::Array(arr) => {
                for element in &arr.elements {
                    self.compile_expression(element)?;
                }
                self.emit(Opcode::Array, &[arr.elements.len()], span);
                Ok(())
            }
            Expr::Map(map) => {
                for (key, value) in &map.pairs {
                    self.compile_expression(key)?;
                    self.compile_expression(value)?;
                }
                self.emit(Opcode::Map, &[map.pairs.len() * 2], span);
                Ok(())
            }
            Expr::Index(index_expr) => {
                self.compile_expression(&index_expr.expr)?;
                self.compile_expression(&index_expr.index)?;
                self.emit(Opcode::Index, &[], span);
                Ok(())
            }
            Expr::Lambda(lambda) => {
                self.compile_fn_body(
                    None,
                    lambda.param_names(),
                    lambda.params.len(),
                    &lambda.body,
                    lambda.span,
                )?;
                Ok(())
            }
            Expr::Call(call) => {
                self.compile_expression(&call.function)?;
                for arg in &call.arguments {
                    self.compile_expression(arg)?;
                }
                self.emit(Opcode::Call, &[call.arguments.len()], span);
                Ok(())
            }
            Expr::Cast(cast) => {
                self.compile_expression(&cast.expr)?;
                let tag = TypeTag::from_cast_target(cast.target);
                self.emit(Opcode::Convert, &[tag.to_byte() as usize], span);
                Ok(())
            }
            Expr::Break(break_expr) => self.compile_break(break_expr),
            Expr::Continue(cont_expr) => self.compile_continue(cont_expr),
            Expr::MacroCall(mc) => self.compile_macro_call(mc),
            Expr::MacroLit(_) => Err(CompileErrorKind::UnsupportedExpr {
                expr_type: "macro literal".to_string(),
            }
            .at(span)
            .into()),
            Expr::StructLit(struct_lit) => self.compile_struct_literal(struct_lit),
            Expr::PathExpr(path_expr) => self.compile_path_expression(path_expr),
            Expr::Try(try_expr) => self.compile_try(try_expr),
            Expr::Match(match_expr) => self.compile_match(match_expr),
            Expr::FieldAccess(field_access) => self.compile_field_access(field_access),
            Expr::MethodCall(method_call) => self.compile_method_call(method_call),
            Expr::Range(range) => {
                self.compile_expression(&range.start)?;
                self.compile_expression(&range.end)?;
                let opcode = if range.inclusive {
                    Opcode::MakeRangeInclusive
                } else {
                    Opcode::MakeRange
                };
                self.emit(opcode, &[], span);
                Ok(())
            }
        }
    }

    /// Compiles a `break` expression inside a loop.
    fn compile_break(&mut self, expr: &BreakExpr) -> Result<()> {
        if self.loop_contexts.is_empty() {
            return Err(CompileErrorKind::BreakOutsideLoop.at(expr.span).into());
        }
        let ctx_index = self.resolve_loop_label(&expr.label, expr.span)?;
        match &expr.value {
            Some(val) => self.compile_expression(val)?,
            None => {
                self.emit(Opcode::Unit, &[], expr.span);
            }
        }
        let jump_pos = self.emit(Opcode::Jump, &[Self::JUMP], expr.span);
        self.loop_contexts[ctx_index].break_jumps.push(jump_pos);
        Ok(())
    }

    /// Compiles a `continue` expression inside a loop.
    fn compile_continue(&mut self, expr: &ContinueExpr) -> Result<()> {
        if self.loop_contexts.is_empty() {
            return Err(CompileErrorKind::ContinueOutsideLoop.at(expr.span).into());
        }
        let ctx_index = self.resolve_loop_label(&expr.label, expr.span)?;
        match self.loop_contexts[ctx_index].continue_target {
            Some(target) => {
                self.emit(Opcode::Jump, &[target], expr.span);
            }
            None => {
                let jump_pos = self.emit(Opcode::Jump, &[Self::JUMP], expr.span);
                self.loop_contexts[ctx_index].continue_jumps.push(jump_pos);
            }
        }
        Ok(())
    }

    /// Resolves a loop label to the index into `self.loop_contexts`.
    ///
    /// Returns the innermost loop's index when no label is specified.
    /// When a label is given, searches outward for a matching loop context.
    fn resolve_loop_label(&self, label: &Option<String>, span: Span) -> Result<usize> {
        match label {
            None => Ok(self.loop_contexts.len() - 1),
            Some(name) => self
                .loop_contexts
                .iter()
                .rposition(|ctx| ctx.label.as_deref() == Some(name))
                .ok_or_else(|| {
                    CompileErrorKind::UndeclaredLabel {
                        label: name.clone(),
                    }
                    .at(span)
                    .into()
                }),
        }
    }

    /// Compiles an infix (binary) expression, including short-circuit `&&`/`||`.
    fn compile_infix(&mut self, infix_expr: &InfixExpr, span: Span) -> Result<()> {
        match infix_expr.operator.as_str() {
            "&&" => {
                self.compile_expression(&infix_expr.lhs)?;
                let cond_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);
                self.compile_expression(&infix_expr.rhs)?;
                let end_jump = self.emit(Opcode::Jump, &[Self::JUMP], span);

                let false_pos = self.current_instructions().len();
                self.replace_operand(cond_jump, false_pos)?;
                self.emit(Opcode::False, &[], span);
                let end_pos = self.current_instructions().len();

                self.replace_operand(end_jump, end_pos)?;
                Ok(())
            }
            "||" => {
                self.compile_expression(&infix_expr.lhs)?;
                let cond_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);
                self.emit(Opcode::True, &[], span);
                let end_jump = self.emit(Opcode::Jump, &[Self::JUMP], span);

                let rhs_pos = self.current_instructions().len();
                self.replace_operand(cond_jump, rhs_pos)?;
                self.compile_expression(&infix_expr.rhs)?;
                let end_pos = self.current_instructions().len();

                self.replace_operand(end_jump, end_pos)?;
                Ok(())
            }
            _ => {
                self.compile_expression(&infix_expr.lhs)?;
                self.compile_expression(&infix_expr.rhs)?;
                if matches!(infix_expr.op_class, BinOpClass::Felt) {
                    return self.emit_felt_infix(infix_expr.operator.as_str(), span);
                }
                match infix_expr.operator.as_str() {
                    ">=" => {
                        self.emit(Opcode::LessThan, &[], span);
                        self.emit(Opcode::Bang, &[], span);
                    }
                    "<=" => {
                        self.emit(Opcode::GreaterThan, &[], span);
                        self.emit(Opcode::Bang, &[], span);
                    }
                    op => {
                        let opcode = match op {
                            "+" => Opcode::Add,
                            "-" => Opcode::Sub,
                            "*" => Opcode::Mul,
                            "/" => Opcode::Div,
                            "%" => Opcode::Mod,
                            ">" => Opcode::GreaterThan,
                            "<" => Opcode::LessThan,
                            "==" => Opcode::Equal,
                            "!=" => Opcode::NotEqual,
                            "&" => Opcode::BitAnd,
                            "|" => Opcode::BitOr,
                            "^" => Opcode::BitXor,
                            "<<" => Opcode::Shl,
                            ">>" => Opcode::Shr,
                            _ => {
                                return Err(CompileErrorKind::UnsupportedOperator {
                                    operator: op.to_string(),
                                    context: "infix expression".to_string(),
                                }
                                .at(span)
                                .into());
                            }
                        };
                        self.emit(opcode, &[], span);
                    }
                }
                Ok(())
            }
        }
    }

    /// Emits the opcode sequence for a `Felt`-typed infix operator. The operands
    /// have already been pushed onto the stack by the caller.
    fn emit_felt_infix(&mut self, op: &str, span: Span) -> Result<()> {
        match op {
            "+" => {
                self.emit(Opcode::FeltAdd, &[], span);
            }
            "-" => {
                self.emit(Opcode::FeltSub, &[], span);
            }
            "*" => {
                self.emit(Opcode::FeltMul, &[], span);
            }
            "/" => {
                self.emit(Opcode::FeltInv, &[], span);
                self.emit(Opcode::FeltMul, &[], span);
            }
            "==" => {
                self.emit(Opcode::Equal, &[], span);
            }
            "!=" => {
                self.emit(Opcode::NotEqual, &[], span);
            }
            other => {
                return Err(CompileErrorKind::UnsupportedOperator {
                    operator: other.to_string(),
                    context: "Felt infix expression".to_string(),
                }
                .at(span)
                .into());
            }
        }
        Ok(())
    }

    /// Compiles an `if`/`else` conditional expression.
    fn compile_conditional(&mut self, cond: &CondExpr) -> Result<()> {
        self.compile_expression(&cond.condition)?;

        let cond_jump_pos = self.emit(Opcode::CondJump, &[Self::JUMP], cond.span);

        self.compile_block_statement(&cond.consequence)?;
        if self.last_instruction_is(Opcode::Pop) {
            self.remove_last_pop();
        } else {
            self.emit(Opcode::Unit, &[], cond.span);
        }
        let jump_pos = self.emit(Opcode::Jump, &[Self::JUMP], cond.span);
        let cons_pos = self.current_instructions().len();
        self.replace_operand(cond_jump_pos, cons_pos)?;
        match &cond.alternative {
            None => {
                self.emit(Opcode::Unit, &[], cond.span);
            }
            Some(alt_block) => {
                self.compile_block_statement(alt_block)?;
                if self.last_instruction_is(Opcode::Pop) {
                    self.remove_last_pop();
                } else {
                    self.emit(Opcode::Unit, &[], cond.span);
                }
            }
        }
        let alt_pos = self.current_instructions().len();
        self.replace_operand(jump_pos, alt_pos)?;
        Ok(())
    }

    /// Compiles a struct literal expression (e.g., `Point { x: 1, y: 2 }`)
    /// or with functional update syntax (e.g., `Point { x: 10, ..other }`).
    fn compile_struct_literal(&mut self, lit: &StructLitExpr) -> Result<()> {
        let span = lit.span;
        let (registry_index, field_names) = self
            .type_registry
            .iter()
            .enumerate()
            .find_map(|(i, td)| match td {
                TypeDef::Struct { name, field_names } if name == &lit.name => {
                    Some((i, field_names.clone()))
                }
                _ => None,
            })
            .ok_or_else(|| {
                CompileErrorKind::UndefinedVariable {
                    name: lit.name.clone(),
                }
                .at(span)
            })?;
        let base_sym = lit
            .base
            .as_ref()
            .map(|base_expr| {
                self.compile_expression(base_expr)?;
                let id = self.for_loop_counter;
                self.for_loop_counter += 1;
                let hidden = format!("__struct_base_{id}");
                self.define_and_set(&hidden, false, span)
            })
            .transpose()?;
        for (field_index, field_name) in field_names.iter().enumerate() {
            match lit.fields.iter().find(|(name, _)| name == field_name) {
                Some((_, expr)) => self.compile_expression(expr)?,
                None => {
                    let sym = base_sym.as_ref().ok_or_else(|| {
                        CompileErrorKind::UndefinedVariable {
                            name: format!(
                                "missing field `{}` in struct `{}`",
                                field_name, lit.name
                            ),
                        }
                        .at(span)
                    })?;
                    self.load_symbol(sym, span);
                    self.emit(Opcode::GetField, &[field_index], span);
                }
            }
        }
        let type_index = registry_index << 8;
        self.emit(Opcode::Construct, &[type_index, field_names.len()], span);
        Ok(())
    }

    /// Compiles a path expression (e.g., `Option::None`, `Color::Red`).
    fn compile_path_expression(&mut self, path: &PathExpr) -> Result<()> {
        let span = path.span;
        if path.segments.len() == 2 {
            let type_name = &path.segments[0];
            let variant_name = &path.segments[1];
            if let Some((registry_index, variant_tag, field_count)) =
                self.resolve_enum_variant(type_name, variant_name)
            {
                return self.emit_variant_constructor(
                    registry_index,
                    variant_tag,
                    field_count,
                    span,
                );
            }
            let qualified_name = format!("{type_name}::{variant_name}");
            if let Some(symbol) = self.symbols_table.resolve_symbol(&qualified_name) {
                self.load_symbol(&symbol, span);
                return Ok(());
            }
        }
        let full_name = path.segments.join("::");
        let symbol = self.resolve_or_error(&full_name, span)?;
        self.load_symbol(&symbol, span);
        Ok(())
    }

    /// Emits bytecode to construct an enum variant.
    fn emit_variant_constructor(
        &mut self,
        registry_index: usize,
        variant_tag: usize,
        field_count: usize,
        span: Span,
    ) -> Result<()> {
        let type_index = (registry_index << 8) | (variant_tag & 0xFF);
        if field_count == 0 {
            self.emit(Opcode::Construct, &[type_index, 0], span);
        } else {
            self.enter_scope();
            let mut param_names = Vec::with_capacity(field_count);
            for i in 0..field_count {
                let name = format!("__field_{i}");
                if let Err(e) = self.symbols_table.define_symbol(&name, false) {
                    return Err(self.attach_span(e, span));
                }
                param_names.push(name);
            }
            for name in &param_names {
                let sym = self.resolve_or_error(name, span)?;
                self.load_symbol(&sym, span);
            }
            self.emit(Opcode::Construct, &[type_index, field_count], span);
            self.emit(Opcode::ReturnValue, &[], span);

            let num_locals = self.symbols_table.max_definitions();
            let (instructions, inner_source_map) = self.leave_scope()?;
            let compiled_fn = Value::CompiledFn(CompiledFn {
                instructions: Rc::from(instructions.as_bytes()),
                num_locals,
                num_parameters: field_count,
                source_map: inner_source_map,
            });
            let index = self.add_constant(compiled_fn)?;
            self.emit(Opcode::Closure, &[index, 0], span);
        }
        Ok(())
    }

    /// Resolves an enum variant by type name and variant name.
    ///
    /// Returns `(registry_index, variant_tag, field_count)` if found.
    fn resolve_enum_variant(
        &self,
        type_name: &str,
        variant_name: &str,
    ) -> Option<(usize, usize, usize)> {
        self.type_registry
            .iter()
            .enumerate()
            .find_map(|(i, td)| match td {
                TypeDef::Enum { name, variants } if name == type_name => variants
                    .iter()
                    .enumerate()
                    .find(|(_, v)| v.name == variant_name)
                    .map(|(tag, v)| (i, tag, v.field_count as usize)),
                _ => None,
            })
    }

    /// Compiles a `match` expression as a chain of `MatchTag` / conditional
    /// jump instructions.
    fn compile_match(&mut self, match_expr: &MatchExpr) -> Result<()> {
        let span = match_expr.span;
        self.compile_expression(&match_expr.scrutinee)?;

        let mut end_jumps = Vec::with_capacity(match_expr.arms.len());
        for arm in &match_expr.arms {
            match &arm.pattern {
                Pattern::TupleStruct { path, fields, .. } => {
                    if let Some((_, variant_tag, _)) = self.find_variant_in_registry(path) {
                        let match_tag_pos =
                            self.emit(Opcode::MatchTag, &[variant_tag, Self::JUMP], span);

                        let (nested_positions, scrutinee_var) =
                            self.bind_variant_fields(fields, span)?;
                        end_jumps.push(self.compile_match_arm_body(&arm.body, span)?);

                        if nested_positions.is_empty() {
                            let next_arm = self.current_instructions().len();
                            self.replace_match_tag_target(match_tag_pos, next_arm)?;
                        } else {
                            let cleanup = self.current_instructions().len();
                            for nested_pos in nested_positions {
                                self.replace_match_tag_target(nested_pos, cleanup)?;
                            }
                            self.emit(Opcode::Pop, &[], span);
                            if let Some(ref var_name) = scrutinee_var {
                                let sym = self.resolve_or_error(var_name, span)?;
                                self.load_symbol(&sym, span);
                            }
                            let next_arm = self.current_instructions().len();
                            self.replace_match_tag_target(match_tag_pos, next_arm)?;
                        }
                    }
                }
                Pattern::Ident { name, mutable, .. } if name != "_" => {
                    if let Some((_, variant_tag, _)) = self.find_variant_in_registry(name) {
                        let match_tag_pos =
                            self.emit(Opcode::MatchTag, &[variant_tag, Self::JUMP], span);
                        self.emit(Opcode::Pop, &[], span);
                        end_jumps.push(self.compile_match_arm_body(&arm.body, span)?);
                        let next_arm = self.current_instructions().len();
                        self.replace_match_tag_target(match_tag_pos, next_arm)?;
                    } else {
                        self.define_and_set(name, *mutable, span)?;
                        end_jumps.push(self.compile_match_arm_body(&arm.body, span)?);
                    }
                }
                Pattern::Wildcard(_) | Pattern::Ident { .. } => {
                    self.emit(Opcode::Pop, &[], span);
                    end_jumps.push(self.compile_match_arm_body(&arm.body, span)?);
                }
                Pattern::Tuple(..) => {
                    self.compile_let_destructure(&arm.pattern, span)?;
                    self.emit(Opcode::Pop, &[], span);
                    end_jumps.push(self.compile_match_arm_body(&arm.body, span)?);
                }
                Pattern::Literal(lit_expr) => {
                    self.compile_expression(lit_expr)?;
                    self.emit(Opcode::Equal, &[], span);
                    let cond_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);
                    end_jumps.push(self.compile_match_arm_body(&arm.body, span)?);
                    let next_arm = self.current_instructions().len();
                    self.replace_operand(cond_jump, next_arm)?;
                }
                _ => {
                    self.emit(Opcode::Pop, &[], span);
                    self.emit(Opcode::Unit, &[], span);
                    end_jumps.push(self.emit(Opcode::Jump, &[Self::JUMP], span));
                }
            }
        }

        self.emit(Opcode::Unit, &[], span);
        let exit = self.current_instructions().len();
        for jump_pos in end_jumps {
            self.replace_operand(jump_pos, exit)?;
        }
        Ok(())
    }

    /// Compiles a match arm body and emits the end-jump.
    ///
    /// Returns the jump position for later back-patching.
    fn compile_match_arm_body(&mut self, body: &Expr, span: Span) -> Result<usize> {
        self.compile_expression(body)?;
        if self.last_instruction_is(Opcode::Pop) {
            self.remove_last_pop();
        }
        Ok(self.emit(Opcode::Jump, &[Self::JUMP], span))
    }

    /// Looks up an enum variant by name in the pre-built variant index.
    fn find_variant_in_registry(&self, variant: &str) -> Option<(usize, usize, usize)> {
        self.variant_index
            .get(variant)
            .map(|entry| (entry.registry_index, entry.tag, entry.field_count))
    }

    /// Binds variant payload fields to local variables via `GetField`.
    ///
    /// Returns `(nested_match_positions, scrutinee_var)` where:
    /// - `nested_match_positions` are bytecode offsets of inner `MatchTag`
    ///   instructions whose jump targets must be patched to cleanup code
    /// - `scrutinee_var` is the hidden variable name storing the outer
    ///   scrutinee, needed to restore the stack on inner match failure
    fn bind_variant_fields(
        &mut self,
        fields: &[Pattern],
        span: Span,
    ) -> Result<(Vec<usize>, Option<String>)> {
        let mut nested_match_positions = Vec::new();
        let mut scrutinee_var = None;
        for (i, field) in fields.iter().enumerate() {
            if let Pattern::Ident { name, mutable, .. } = field {
                if i == 0 {
                    let hidden = format!("__match_scrutinee_{}", self.for_loop_counter);
                    self.for_loop_counter += 1;
                    let hidden_sym = self.define_and_set(&hidden, false, span)?;
                    scrutinee_var = Some(hidden.clone());
                    self.load_symbol(&hidden_sym, span);
                    self.emit(Opcode::GetField, &[i], span);
                } else {
                    let hidden = format!("__match_scrutinee_{}", self.for_loop_counter - 1);
                    let hidden_sym = self.resolve_or_error(&hidden, span)?;
                    self.load_symbol(&hidden_sym, span);
                    self.emit(Opcode::GetField, &[i], span);
                }

                if name == "_" {
                    self.emit(Opcode::Pop, &[], span);
                } else if let Some((_, variant_tag, _)) = self.find_variant_in_registry(name) {
                    let pos = self.emit(Opcode::MatchTag, &[variant_tag, Self::JUMP], span);
                    self.emit(Opcode::Pop, &[], span);
                    nested_match_positions.push(pos);
                } else {
                    self.define_and_set(name, *mutable, span)?;
                }
            }
        }
        Ok((nested_match_positions, scrutinee_var))
    }

    /// Replaces the jump-on-mismatch target of a `MatchTag` instruction.
    ///
    /// `MatchTag` has operands `[u16 variant_tag, u16 jump_target]`. The jump
    /// target is at offset `op_pos + 3` (1 opcode + 2 tag bytes).
    fn replace_match_tag_target(&mut self, op_pos: usize, target: usize) -> Result<()> {
        let scope = &mut self.scopes[self.scope_index];
        let target_bytes = (target as u16).to_be_bytes();
        scope.instructions.replace_bytes(op_pos + 3, &target_bytes);
        Ok(())
    }

    /// Compiles a field access expression (e.g., `point.x`).
    fn compile_field_access(&mut self, fa: &FieldAccessExpr) -> Result<()> {
        let span = fa.span;
        self.compile_expression(&fa.object)?;
        let field_index = fa
            .field
            .parse::<usize>()
            .ok()
            .or_else(|| {
                self.type_registry.iter().find_map(|td| match td {
                    TypeDef::Struct { field_names, .. } => {
                        field_names.iter().position(|f| f == &fa.field)
                    }
                    _ => None,
                })
            })
            .ok_or_else(|| {
                CompileErrorKind::UndefinedVariable {
                    name: format!("unknown field `{}`", fa.field),
                }
                .at(span)
            })?;
        self.emit(Opcode::GetField, &[field_index], span);
        Ok(())
    }

    /// Compiles a destructuring `let` binding (e.g., `let (x, y) = expr;`).
    fn compile_let_destructure(&mut self, pattern: &Pattern, span: Span) -> Result<()> {
        match pattern {
            Pattern::Tuple(fields, _) => {
                let temp = self.define_anonymous_local(span)?;
                for (i, field) in fields.iter().enumerate() {
                    match field {
                        Pattern::Ident { name, mutable, .. } => {
                            self.load_symbol(&temp, span);
                            self.emit(Opcode::GetField, &[i], span);
                            self.define_and_set(name, *mutable, span)?;
                        }
                        Pattern::Wildcard(_) => {}
                        Pattern::Tuple(..) => {
                            self.load_symbol(&temp, span);
                            self.emit(Opcode::GetField, &[i], span);
                            self.compile_let_destructure(field, span)?;
                        }
                        _ => {
                            return Err(CompileErrorKind::UnsupportedExpr {
                                expr_type: "unsupported pattern in tuple destructuring".to_string(),
                            }
                            .at(span)
                            .into());
                        }
                    }
                }
                Ok(())
            }
            _ => Err(CompileErrorKind::UnsupportedExpr {
                expr_type: "expected tuple pattern in let destructuring".to_string(),
            }
            .at(span)
            .into()),
        }
    }

    /// Defines an anonymous variable and sets it from the stack top.
    fn define_anonymous_local(&mut self, span: Span) -> Result<Symbol> {
        use std::sync::atomic::{AtomicUsize, Ordering};

        static COUNTER: AtomicUsize = AtomicUsize::new(0);
        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
        let name = format!("__destructure_{id}");
        self.define_and_set(&name, false, span)
    }

    /// Compiles a method call expression (e.g., `point.distance(other)`).
    fn compile_method_call(&mut self, mc: &MethodCallExpr) -> Result<()> {
        let span = mc.span;
        if self.is_desugared_higher_order(mc) {
            return self.compile_desugared_higher_order(mc);
        }
        let qualified_name = self.resolve_method_name(mc).ok_or_else(|| {
            CompileErrorKind::UndefinedVariable {
                name: format!("unknown method `{}`", mc.method),
            }
            .at(span)
        })?;
        let symbol = self.resolve_or_error(&qualified_name, span)?;
        self.load_symbol(&symbol, span);
        self.compile_expression(&mc.object)?;
        for arg in &mc.arguments {
            self.compile_expression(arg)?;
        }
        let total_args = 1 + mc.arguments.len();
        self.emit(Opcode::Call, &[total_args], span);
        Ok(())
    }

    /// Returns `true` if this method call should be desugared into inline
    /// bytecode rather than dispatched as a builtin call.
    ///
    /// Higher-order methods on `Option`, `Result`, and `Vector` accept closures
    /// that require VM-level invocation. These are compiled as inline match +
    /// call sequences instead of builtin function calls.
    fn is_desugared_higher_order(&self, mc: &MethodCallExpr) -> bool {
        matches!(
            (mc.receiver.as_deref(), mc.method.as_str()),
            (Some("Option"), "map" | "and_then" | "unwrap_or_else")
                | (
                    Some("Result"),
                    "map" | "and_then" | "map_err" | "unwrap_or_else" | "or_else"
                )
                | (
                    Some("Vector"),
                    "map"
                        | "filter"
                        | "fold"
                        | "any"
                        | "all"
                        | "find"
                        | "position"
                        | "for_each"
                        | "flat_map"
                )
        )
    }

    /// Dispatches a higher-order method call to the appropriate inline
    /// desugaring (Option/Result or Vector iterators).
    fn compile_desugared_higher_order(&mut self, mc: &MethodCallExpr) -> Result<()> {
        match mc.receiver.as_deref() {
            Some("Option" | "Result") => self.compile_option_result_higher_order(mc),
            Some("Vector") => self.compile_vector_iterator(mc),
            _ => unreachable!("is_desugared_higher_order already validated receiver"),
        }
    }

    /// Compiles higher-order `Option`/`Result` methods as inline bytecode
    /// equivalent to match expressions. Dispatches by method name to the
    /// appropriate pattern.
    fn compile_option_result_higher_order(&mut self, mc: &MethodCallExpr) -> Result<()> {
        match mc.method.as_str() {
            "map" | "and_then" => self.compile_option_result_map_like(mc),
            "unwrap_or_else" => self.compile_option_result_unwrap_or_else(mc),
            "map_err" => self.compile_result_map_err(mc),
            "or_else" => self.compile_result_or_else(mc),
            _ => unreachable!("unhandled Option/Result HOF: {}", mc.method),
        }
    }

    /// Compiles `Option::map`, `Option::and_then`, `Result::map`, or
    /// `Result::and_then` as inline bytecode.
    ///
    /// Pattern: apply `f` to the success payload, pass through the failure arm.
    /// ```text
    /// opt.map(f)      => match opt { Some(x) => Some(f(x)), None => None }
    /// opt.and_then(f) => match opt { Some(x) => f(x),       None => None }
    /// ```
    fn compile_option_result_map_like(&mut self, mc: &MethodCallExpr) -> Result<()> {
        let span = mc.span;
        let is_option = mc.receiver.as_deref() == Some("Option");
        let is_map = mc.method == "map";

        let success_tag: usize = 0;
        let type_index: usize = if is_option { 0 } else { 1 };
        let some_or_ok = type_index << 8;
        let none_or_err = (type_index << 8) | 1;

        let id = self.for_loop_counter;
        self.for_loop_counter += 1;
        let fn_name = format!("__hof_fn_{id}");
        let val_name = format!("__hof_val_{id}");

        self.compile_expression(&mc.arguments[0])?;
        let fn_sym = self.define_and_set(&fn_name, false, span)?;

        self.compile_expression(&mc.object)?;

        let match_tag_pos = self.emit(Opcode::MatchTag, &[success_tag, Self::JUMP], span);

        self.emit(Opcode::GetField, &[0], span);
        let val_sym = self.define_and_set(&val_name, false, span)?;

        self.load_symbol(&fn_sym, span);
        self.load_symbol(&val_sym, span);
        self.emit(Opcode::Call, &[1], span);

        if is_map {
            self.emit(Opcode::Construct, &[some_or_ok, 1], span);
        }
        let jump_to_end = self.emit(Opcode::Jump, &[Self::JUMP], span);

        let fail_arm = self.current_instructions().len();
        self.replace_match_tag_target(match_tag_pos, fail_arm)?;

        if is_option {
            self.emit(Opcode::Pop, &[], span);
            self.emit(Opcode::Construct, &[none_or_err, 0], span);
        } else {
            // `Result::Err`: the scrutinee is already the Err variant; keep it.
        }
        let end = self.current_instructions().len();
        self.replace_operand(jump_to_end, end)?;
        Ok(())
    }

    /// Compiles `Option::unwrap_or_else(f)` and `Result::unwrap_or_else(f)`.
    ///
    /// ```text
    /// opt.unwrap_or_else(f) => match opt { Some(x) => x, None => f() }
    /// res.unwrap_or_else(f) => match res { Ok(x) => x,   Err(e) => f(e) }
    /// ```
    fn compile_option_result_unwrap_or_else(&mut self, mc: &MethodCallExpr) -> Result<()> {
        let span = mc.span;
        let is_option = mc.receiver.as_deref() == Some("Option");
        let success_tag: usize = 0;

        let id = self.for_loop_counter;
        self.for_loop_counter += 1;
        let fn_name = format!("__hof_fn_{id}");
        let val_name = format!("__hof_val_{id}");

        self.compile_expression(&mc.arguments[0])?;
        let fn_sym = self.define_and_set(&fn_name, false, span)?;

        self.compile_expression(&mc.object)?;

        let match_tag_pos = self.emit(Opcode::MatchTag, &[success_tag, Self::JUMP], span);
        self.emit(Opcode::GetField, &[0], span);
        let jump_to_end = self.emit(Opcode::Jump, &[Self::JUMP], span);

        let fail_arm = self.current_instructions().len();
        self.replace_match_tag_target(match_tag_pos, fail_arm)?;

        if is_option {
            self.emit(Opcode::Pop, &[], span);
            self.load_symbol(&fn_sym, span);
            self.emit(Opcode::Call, &[0], span);
        } else {
            self.emit(Opcode::GetField, &[0], span);
            let val_sym = self.define_and_set(&val_name, false, span)?;
            self.load_symbol(&fn_sym, span);
            self.load_symbol(&val_sym, span);
            self.emit(Opcode::Call, &[1], span);
        }

        let end = self.current_instructions().len();
        self.replace_operand(jump_to_end, end)?;
        Ok(())
    }

    /// Compiles `Result::map_err(f)`.
    ///
    /// ```text
    /// res.map_err(f) => match res { Ok(x) => Ok(x), Err(e) => Err(f(e)) }
    /// ```
    fn compile_result_map_err(&mut self, mc: &MethodCallExpr) -> Result<()> {
        let span = mc.span;
        let ok_tag: usize = 0;
        let err_construct: usize = (1 << 8) | 1; // Result type_index=1, Err tag=1

        let id = self.for_loop_counter;
        self.for_loop_counter += 1;
        let fn_name = format!("__hof_fn_{id}");
        let val_name = format!("__hof_val_{id}");

        self.compile_expression(&mc.arguments[0])?;
        let fn_sym = self.define_and_set(&fn_name, false, span)?;

        self.compile_expression(&mc.object)?;

        let match_tag_pos = self.emit(Opcode::MatchTag, &[ok_tag, Self::JUMP], span);
        let jump_to_end = self.emit(Opcode::Jump, &[Self::JUMP], span);

        let err_arm = self.current_instructions().len();
        self.replace_match_tag_target(match_tag_pos, err_arm)?;

        self.emit(Opcode::GetField, &[0], span);
        let val_sym = self.define_and_set(&val_name, false, span)?;

        self.load_symbol(&fn_sym, span);
        self.load_symbol(&val_sym, span);
        self.emit(Opcode::Call, &[1], span);
        self.emit(Opcode::Construct, &[err_construct, 1], span);

        let end = self.current_instructions().len();
        self.replace_operand(jump_to_end, end)?;
        Ok(())
    }

    /// Compiles `Result::or_else(f)`.
    ///
    /// ```text
    /// res.or_else(f) => match res { Ok(x) => Ok(x), Err(e) => f(e) }
    /// ```
    fn compile_result_or_else(&mut self, mc: &MethodCallExpr) -> Result<()> {
        let span = mc.span;
        let ok_tag: usize = 0;

        let id = self.for_loop_counter;
        self.for_loop_counter += 1;
        let fn_name = format!("__hof_fn_{id}");
        let val_name = format!("__hof_val_{id}");

        self.compile_expression(&mc.arguments[0])?;
        let fn_sym = self.define_and_set(&fn_name, false, span)?;

        self.compile_expression(&mc.object)?;

        let match_tag_pos = self.emit(Opcode::MatchTag, &[ok_tag, Self::JUMP], span);
        let jump_to_end = self.emit(Opcode::Jump, &[Self::JUMP], span);

        let err_arm = self.current_instructions().len();
        self.replace_match_tag_target(match_tag_pos, err_arm)?;

        self.emit(Opcode::GetField, &[0], span);
        let val_sym = self.define_and_set(&val_name, false, span)?;

        self.load_symbol(&fn_sym, span);
        self.load_symbol(&val_sym, span);
        self.emit(Opcode::Call, &[1], span);

        let end = self.current_instructions().len();
        self.replace_operand(jump_to_end, end)?;
        Ok(())
    }

    /// Compiles `Vector::map`, `Vector::filter`, `Vector::fold`, `Vector::any`,
    /// and `Vector::all` as inline loop desugaring.
    ///
    /// Each method desugars to a counting loop over the source vector:
    ///
    /// - `map(f)`:   builds a new vector by applying `f` to each element
    /// - `filter(f)`: builds a new vector of elements where `f` returns true
    /// - `fold(init, f)`: accumulates a single value via `f(acc, elem)`
    /// - `any(f)`:   short-circuits to `true` when `f(elem)` is true
    /// - `all(f)`:   short-circuits to `false` when `f(elem)` is false
    fn compile_vector_iterator(&mut self, mc: &MethodCallExpr) -> Result<()> {
        match mc.method.as_str() {
            "map" => self.compile_vector_map(mc),
            "filter" => self.compile_vector_filter(mc),
            "fold" => self.compile_vector_fold(mc),
            "any" => self.compile_vector_any_all(mc, true),
            "all" => self.compile_vector_any_all(mc, false),
            "find" => self.compile_vector_find(mc),
            "position" => self.compile_vector_position(mc),
            "for_each" => self.compile_vector_for_each(mc),
            "flat_map" => self.compile_vector_flat_map(mc),
            _ => unreachable!("is_desugared_higher_order already validated method"),
        }
    }

    /// Compiles `vec.map(f)` as a loop that builds a new vector.
    ///
    /// ```text
    /// let __fn = f;
    /// let __iter = vec;
    /// let __len = Vector::len(__iter);
    /// let __result = [];
    /// let __i = 0;
    /// loop_start:
    ///   if __i >= __len: jump exit
    ///   let __elem = __iter[__i]
    ///   __result = Vector::push(__result, __fn(__elem))
    ///   __i += 1
    ///   jump loop_start
    /// exit:
    ///   __result
    /// ```
    fn compile_vector_map(&mut self, mc: &MethodCallExpr) -> Result<()> {
        let span = mc.span;
        let id = self.for_loop_counter;
        self.for_loop_counter += 1;

        let fn_name = format!("__vmap_fn_{id}");
        let iter_name = format!("__vmap_iter_{id}");
        let len_name = format!("__vmap_len_{id}");
        let result_name = format!("__vmap_result_{id}");
        let i_name = format!("__vmap_i_{id}");
        let elem_name = format!("__vmap_elem_{id}");

        self.compile_expression(&mc.arguments[0])?;
        let fn_sym = self.define_and_set(&fn_name, false, span)?;

        self.compile_expression(&mc.object)?;
        let iter_sym = self.define_and_set(&iter_name, false, span)?;

        let len_builtin = self.resolve_or_error("Vector::len", span)?;
        self.load_symbol(&len_builtin, span);
        self.load_symbol(&iter_sym, span);
        self.emit(Opcode::Call, &[1], span);
        let len_sym = self.define_and_set(&len_name, false, span)?;

        self.emit(Opcode::Vector, &[0], span);
        let result_sym = self.define_and_set(&result_name, true, span)?;

        let zero_idx = self.add_constant(Value::Integer(Integer::I64(0)))?;
        self.emit(Opcode::Constant, &[zero_idx], span);
        let i_sym = self.define_and_set(&i_name, true, span)?;

        let loop_start = self.current_instructions().len();
        self.load_symbol(&i_sym, span);
        self.load_symbol(&len_sym, span);
        self.emit(Opcode::LessThan, &[], span);
        let exit_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.load_symbol(&iter_sym, span);
        self.load_symbol(&i_sym, span);
        self.emit(Opcode::Index, &[], span);
        let elem_sym = self.define_and_set(&elem_name, false, span)?;

        let push_builtin = self.resolve_or_error("Vector::push", span)?;
        self.load_symbol(&push_builtin, span);
        self.load_symbol(&result_sym, span);
        self.load_symbol(&fn_sym, span);
        self.load_symbol(&elem_sym, span);
        self.emit(Opcode::Call, &[1], span);
        self.emit(Opcode::Call, &[2], span);
        self.emit_set_symbol(&result_sym, span);

        self.load_symbol(&i_sym, span);
        let one_idx = self.add_constant(Value::Integer(Integer::I64(1)))?;
        self.emit(Opcode::Constant, &[one_idx], span);
        self.emit(Opcode::Add, &[], span);
        self.emit_set_symbol(&i_sym, span);

        self.emit(Opcode::Jump, &[loop_start], span);

        let loop_exit = self.current_instructions().len();
        self.replace_operand(exit_jump, loop_exit)?;
        self.load_symbol(&result_sym, span);
        Ok(())
    }

    /// Compiles `vec.filter(f)` as a loop that conditionally appends elements.
    ///
    /// ```text
    /// let __fn = f;
    /// let __iter = vec;
    /// let __len = Vector::len(__iter);
    /// let __result = [];
    /// let __i = 0;
    /// loop_start:
    ///   if __i >= __len: jump exit
    ///   let __elem = __iter[__i]
    ///   if !__fn(__elem): jump skip
    ///   __result = Vector::push(__result, __elem)
    /// skip:
    ///   __i += 1
    ///   jump loop_start
    /// exit:
    ///   __result
    /// ```
    fn compile_vector_filter(&mut self, mc: &MethodCallExpr) -> Result<()> {
        let span = mc.span;
        let id = self.for_loop_counter;
        self.for_loop_counter += 1;

        let fn_name = format!("__vfilt_fn_{id}");
        let iter_name = format!("__vfilt_iter_{id}");
        let len_name = format!("__vfilt_len_{id}");
        let result_name = format!("__vfilt_result_{id}");
        let i_name = format!("__vfilt_i_{id}");
        let elem_name = format!("__vfilt_elem_{id}");

        self.compile_expression(&mc.arguments[0])?;
        let fn_sym = self.define_and_set(&fn_name, false, span)?;

        self.compile_expression(&mc.object)?;
        let iter_sym = self.define_and_set(&iter_name, false, span)?;

        let len_builtin = self.resolve_or_error("Vector::len", span)?;
        self.load_symbol(&len_builtin, span);
        self.load_symbol(&iter_sym, span);
        self.emit(Opcode::Call, &[1], span);
        let len_sym = self.define_and_set(&len_name, false, span)?;

        self.emit(Opcode::Vector, &[0], span);
        let result_sym = self.define_and_set(&result_name, true, span)?;

        let zero_idx = self.add_constant(Value::Integer(Integer::I64(0)))?;
        self.emit(Opcode::Constant, &[zero_idx], span);
        let i_sym = self.define_and_set(&i_name, true, span)?;

        let loop_start = self.current_instructions().len();
        self.load_symbol(&i_sym, span);
        self.load_symbol(&len_sym, span);
        self.emit(Opcode::LessThan, &[], span);
        let exit_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.load_symbol(&iter_sym, span);
        self.load_symbol(&i_sym, span);
        self.emit(Opcode::Index, &[], span);
        let elem_sym = self.define_and_set(&elem_name, false, span)?;

        self.load_symbol(&fn_sym, span);
        self.load_symbol(&elem_sym, span);
        self.emit(Opcode::Call, &[1], span);
        let skip_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        let push_builtin = self.resolve_or_error("Vector::push", span)?;
        self.load_symbol(&push_builtin, span);
        self.load_symbol(&result_sym, span);
        self.load_symbol(&elem_sym, span);
        self.emit(Opcode::Call, &[2], span);
        self.emit_set_symbol(&result_sym, span);

        let skip_target = self.current_instructions().len();
        self.replace_operand(skip_jump, skip_target)?;

        self.load_symbol(&i_sym, span);
        let one_idx = self.add_constant(Value::Integer(Integer::I64(1)))?;
        self.emit(Opcode::Constant, &[one_idx], span);
        self.emit(Opcode::Add, &[], span);
        self.emit_set_symbol(&i_sym, span);

        self.emit(Opcode::Jump, &[loop_start], span);

        let loop_exit = self.current_instructions().len();
        self.replace_operand(exit_jump, loop_exit)?;
        self.load_symbol(&result_sym, span);
        Ok(())
    }

    /// Compiles `vec.fold(init, f)` as a loop that accumulates a value.
    ///
    /// ```text
    /// let __fn = f;
    /// let __acc = init;
    /// let __iter = vec;
    /// let __len = Vector::len(__iter);
    /// let __i = 0;
    /// loop_start:
    ///   if __i >= __len: jump exit
    ///   let __elem = __iter[__i]
    ///   __acc = __fn(__acc, __elem)
    ///   __i += 1
    ///   jump loop_start
    /// exit:
    ///   __acc
    /// ```
    fn compile_vector_fold(&mut self, mc: &MethodCallExpr) -> Result<()> {
        let span = mc.span;
        let id = self.for_loop_counter;
        self.for_loop_counter += 1;

        let fn_name = format!("__vfold_fn_{id}");
        let acc_name = format!("__vfold_acc_{id}");
        let iter_name = format!("__vfold_iter_{id}");
        let len_name = format!("__vfold_len_{id}");
        let i_name = format!("__vfold_i_{id}");
        let elem_name = format!("__vfold_elem_{id}");

        self.compile_expression(&mc.arguments[1])?;
        let fn_sym = self.define_and_set(&fn_name, false, span)?;

        self.compile_expression(&mc.arguments[0])?;
        let acc_sym = self.define_and_set(&acc_name, true, span)?;

        self.compile_expression(&mc.object)?;
        let iter_sym = self.define_and_set(&iter_name, false, span)?;

        let len_builtin = self.resolve_or_error("Vector::len", span)?;
        self.load_symbol(&len_builtin, span);
        self.load_symbol(&iter_sym, span);
        self.emit(Opcode::Call, &[1], span);
        let len_sym = self.define_and_set(&len_name, false, span)?;

        let zero_idx = self.add_constant(Value::Integer(Integer::I64(0)))?;
        self.emit(Opcode::Constant, &[zero_idx], span);
        let i_sym = self.define_and_set(&i_name, true, span)?;

        let loop_start = self.current_instructions().len();
        self.load_symbol(&i_sym, span);
        self.load_symbol(&len_sym, span);
        self.emit(Opcode::LessThan, &[], span);
        let exit_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.load_symbol(&iter_sym, span);
        self.load_symbol(&i_sym, span);
        self.emit(Opcode::Index, &[], span);
        let elem_sym = self.define_and_set(&elem_name, false, span)?;

        self.load_symbol(&fn_sym, span);
        self.load_symbol(&acc_sym, span);
        self.load_symbol(&elem_sym, span);
        self.emit(Opcode::Call, &[2], span);
        self.emit_set_symbol(&acc_sym, span);

        self.load_symbol(&i_sym, span);
        let one_idx = self.add_constant(Value::Integer(Integer::I64(1)))?;
        self.emit(Opcode::Constant, &[one_idx], span);
        self.emit(Opcode::Add, &[], span);
        self.emit_set_symbol(&i_sym, span);

        self.emit(Opcode::Jump, &[loop_start], span);

        let loop_exit = self.current_instructions().len();
        self.replace_operand(exit_jump, loop_exit)?;
        self.load_symbol(&acc_sym, span);
        Ok(())
    }

    /// Compiles `vec.any(f)` and `vec.all(f)` as short-circuiting loops.
    ///
    /// For `any`: returns `true` as soon as `f(elem)` is `true`, else `false`.
    /// For `all`: returns `false` as soon as `f(elem)` is `false`, else `true`.
    ///
    /// ```text
    /// let __fn = f;
    /// let __iter = vec;
    /// let __len = Vector::len(__iter);
    /// let __i = 0;
    /// loop_start:
    ///   if __i >= __len: jump default_result
    ///   let __elem = __iter[__i]
    ///   let __test = __fn(__elem)
    ///   // any: if __test => jump early_result
    ///   // all: if !__test => jump early_result
    ///   __i += 1
    ///   jump loop_start
    /// early_result:
    ///   push <early_value>  // any: true, all: false
    ///   jump end
    /// default_result:
    ///   push <default_value>  // any: false, all: true
    /// end:
    /// ```
    fn compile_vector_any_all(&mut self, mc: &MethodCallExpr, is_any: bool) -> Result<()> {
        let span = mc.span;
        let id = self.for_loop_counter;
        self.for_loop_counter += 1;

        let fn_name = format!("__vaa_fn_{id}");
        let iter_name = format!("__vaa_iter_{id}");
        let len_name = format!("__vaa_len_{id}");
        let i_name = format!("__vaa_i_{id}");
        let elem_name = format!("__vaa_elem_{id}");

        self.compile_expression(&mc.arguments[0])?;
        let fn_sym = self.define_and_set(&fn_name, false, span)?;

        self.compile_expression(&mc.object)?;
        let iter_sym = self.define_and_set(&iter_name, false, span)?;

        let len_builtin = self.resolve_or_error("Vector::len", span)?;
        self.load_symbol(&len_builtin, span);
        self.load_symbol(&iter_sym, span);
        self.emit(Opcode::Call, &[1], span);
        let len_sym = self.define_and_set(&len_name, false, span)?;

        let zero_idx = self.add_constant(Value::Integer(Integer::I64(0)))?;
        self.emit(Opcode::Constant, &[zero_idx], span);
        let i_sym = self.define_and_set(&i_name, true, span)?;

        let loop_start = self.current_instructions().len();
        self.load_symbol(&i_sym, span);
        self.load_symbol(&len_sym, span);
        self.emit(Opcode::LessThan, &[], span);
        let exit_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.load_symbol(&iter_sym, span);
        self.load_symbol(&i_sym, span);
        self.emit(Opcode::Index, &[], span);
        let elem_sym = self.define_and_set(&elem_name, false, span)?;

        self.load_symbol(&fn_sym, span);
        self.load_symbol(&elem_sym, span);
        self.emit(Opcode::Call, &[1], span);

        let (early_value, default_value) = if is_any { (true, false) } else { (false, true) };

        if is_any {
            let continue_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

            let early_idx = self.add_constant(Value::Bool(early_value))?;
            self.emit(Opcode::Constant, &[early_idx], span);
            let early_exit = self.emit(Opcode::Jump, &[Self::JUMP], span);

            let continue_target = self.current_instructions().len();
            self.replace_operand(continue_jump, continue_target)?;

            self.load_symbol(&i_sym, span);
            let one_idx = self.add_constant(Value::Integer(Integer::I64(1)))?;
            self.emit(Opcode::Constant, &[one_idx], span);
            self.emit(Opcode::Add, &[], span);
            self.emit_set_symbol(&i_sym, span);
            self.emit(Opcode::Jump, &[loop_start], span);

            let default_target = self.current_instructions().len();
            self.replace_operand(exit_jump, default_target)?;
            let default_idx = self.add_constant(Value::Bool(default_value))?;
            self.emit(Opcode::Constant, &[default_idx], span);

            let end = self.current_instructions().len();
            self.replace_operand(early_exit, end)?;
        } else {
            let early_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

            self.load_symbol(&i_sym, span);
            let one_idx = self.add_constant(Value::Integer(Integer::I64(1)))?;
            self.emit(Opcode::Constant, &[one_idx], span);
            self.emit(Opcode::Add, &[], span);
            self.emit_set_symbol(&i_sym, span);
            self.emit(Opcode::Jump, &[loop_start], span);

            let early_target = self.current_instructions().len();
            self.replace_operand(early_jump, early_target)?;
            let early_idx = self.add_constant(Value::Bool(early_value))?;
            self.emit(Opcode::Constant, &[early_idx], span);
            let early_exit = self.emit(Opcode::Jump, &[Self::JUMP], span);

            let default_target = self.current_instructions().len();
            self.replace_operand(exit_jump, default_target)?;
            let default_idx = self.add_constant(Value::Bool(default_value))?;
            self.emit(Opcode::Constant, &[default_idx], span);

            let end = self.current_instructions().len();
            self.replace_operand(early_exit, end)?;
        }
        Ok(())
    }

    /// Compiles `vec.find(f)`, returning `Some(elem)` for the first element
    /// where `f(elem)` is true, or `None` if no match.
    fn compile_vector_find(&mut self, mc: &MethodCallExpr) -> Result<()> {
        let span = mc.span;
        let id = self.for_loop_counter;
        self.for_loop_counter += 1;

        let fn_name = format!("__vfind_fn_{id}");
        let iter_name = format!("__vfind_iter_{id}");
        let len_name = format!("__vfind_len_{id}");
        let i_name = format!("__vfind_i_{id}");
        let elem_name = format!("__vfind_elem_{id}");

        self.compile_expression(&mc.arguments[0])?;
        let fn_sym = self.define_and_set(&fn_name, false, span)?;

        self.compile_expression(&mc.object)?;
        let iter_sym = self.define_and_set(&iter_name, false, span)?;

        let len_builtin = self.resolve_or_error("Vector::len", span)?;
        self.load_symbol(&len_builtin, span);
        self.load_symbol(&iter_sym, span);
        self.emit(Opcode::Call, &[1], span);
        let len_sym = self.define_and_set(&len_name, false, span)?;

        let zero_idx = self.add_constant(Value::Integer(Integer::I64(0)))?;
        self.emit(Opcode::Constant, &[zero_idx], span);
        let i_sym = self.define_and_set(&i_name, true, span)?;

        let loop_start = self.current_instructions().len();
        self.load_symbol(&i_sym, span);
        self.load_symbol(&len_sym, span);
        self.emit(Opcode::LessThan, &[], span);
        let exit_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.load_symbol(&iter_sym, span);
        self.load_symbol(&i_sym, span);
        self.emit(Opcode::Index, &[], span);
        let elem_sym = self.define_and_set(&elem_name, false, span)?;

        self.load_symbol(&fn_sym, span);
        self.load_symbol(&elem_sym, span);
        self.emit(Opcode::Call, &[1], span);

        let continue_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.load_symbol(&elem_sym, span);
        let some_tag = 0usize; // Option type_index=0, Some tag=0
        self.emit(Opcode::Construct, &[some_tag, 1], span);
        let early_exit = self.emit(Opcode::Jump, &[Self::JUMP], span);

        let continue_target = self.current_instructions().len();
        self.replace_operand(continue_jump, continue_target)?;

        self.load_symbol(&i_sym, span);
        let one_idx = self.add_constant(Value::Integer(Integer::I64(1)))?;
        self.emit(Opcode::Constant, &[one_idx], span);
        self.emit(Opcode::Add, &[], span);
        self.emit_set_symbol(&i_sym, span);
        self.emit(Opcode::Jump, &[loop_start], span);

        let default_target = self.current_instructions().len();
        self.replace_operand(exit_jump, default_target)?;
        let none_tag = 1usize; // Option None tag=1
        self.emit(Opcode::Construct, &[none_tag, 0], span);

        let end = self.current_instructions().len();
        self.replace_operand(early_exit, end)?;
        Ok(())
    }

    /// Compiles `vec.position(f)`, returning `Some(index)` for the first element
    /// where `f(elem)` is true, or `None` if no match.
    fn compile_vector_position(&mut self, mc: &MethodCallExpr) -> Result<()> {
        let span = mc.span;
        let id = self.for_loop_counter;
        self.for_loop_counter += 1;

        let fn_name = format!("__vpos_fn_{id}");
        let iter_name = format!("__vpos_iter_{id}");
        let len_name = format!("__vpos_len_{id}");
        let i_name = format!("__vpos_i_{id}");
        let elem_name = format!("__vpos_elem_{id}");

        self.compile_expression(&mc.arguments[0])?;
        let fn_sym = self.define_and_set(&fn_name, false, span)?;

        self.compile_expression(&mc.object)?;
        let iter_sym = self.define_and_set(&iter_name, false, span)?;

        let len_builtin = self.resolve_or_error("Vector::len", span)?;
        self.load_symbol(&len_builtin, span);
        self.load_symbol(&iter_sym, span);
        self.emit(Opcode::Call, &[1], span);
        let len_sym = self.define_and_set(&len_name, false, span)?;

        let zero_idx = self.add_constant(Value::Integer(Integer::Usize(0)))?;
        self.emit(Opcode::Constant, &[zero_idx], span);
        let i_sym = self.define_and_set(&i_name, true, span)?;

        let loop_start = self.current_instructions().len();
        self.load_symbol(&i_sym, span);
        self.load_symbol(&len_sym, span);
        self.emit(Opcode::LessThan, &[], span);
        let exit_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.load_symbol(&iter_sym, span);
        self.load_symbol(&i_sym, span);
        self.emit(Opcode::Index, &[], span);
        let elem_sym = self.define_and_set(&elem_name, false, span)?;

        self.load_symbol(&fn_sym, span);
        self.load_symbol(&elem_sym, span);
        self.emit(Opcode::Call, &[1], span);

        let continue_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.load_symbol(&i_sym, span);
        let some_tag = 0usize;
        self.emit(Opcode::Construct, &[some_tag, 1], span);
        let early_exit = self.emit(Opcode::Jump, &[Self::JUMP], span);

        let continue_target = self.current_instructions().len();
        self.replace_operand(continue_jump, continue_target)?;

        self.load_symbol(&i_sym, span);
        let one_idx = self.add_constant(Value::Integer(Integer::Usize(1)))?;
        self.emit(Opcode::Constant, &[one_idx], span);
        self.emit(Opcode::Add, &[], span);
        self.emit_set_symbol(&i_sym, span);
        self.emit(Opcode::Jump, &[loop_start], span);

        let default_target = self.current_instructions().len();
        self.replace_operand(exit_jump, default_target)?;
        let none_tag = 1usize;
        self.emit(Opcode::Construct, &[none_tag, 0], span);

        let end = self.current_instructions().len();
        self.replace_operand(early_exit, end)?;
        Ok(())
    }

    /// Compiles `vec.for_each(f)`, applying `f` to each element and discarding
    /// results. Returns unit.
    fn compile_vector_for_each(&mut self, mc: &MethodCallExpr) -> Result<()> {
        let span = mc.span;
        let id = self.for_loop_counter;
        self.for_loop_counter += 1;

        let fn_name = format!("__vfe_fn_{id}");
        let iter_name = format!("__vfe_iter_{id}");
        let len_name = format!("__vfe_len_{id}");
        let i_name = format!("__vfe_i_{id}");
        let elem_name = format!("__vfe_elem_{id}");

        self.compile_expression(&mc.arguments[0])?;
        let fn_sym = self.define_and_set(&fn_name, false, span)?;

        self.compile_expression(&mc.object)?;
        let iter_sym = self.define_and_set(&iter_name, false, span)?;

        let len_builtin = self.resolve_or_error("Vector::len", span)?;
        self.load_symbol(&len_builtin, span);
        self.load_symbol(&iter_sym, span);
        self.emit(Opcode::Call, &[1], span);
        let len_sym = self.define_and_set(&len_name, false, span)?;

        let zero_idx = self.add_constant(Value::Integer(Integer::I64(0)))?;
        self.emit(Opcode::Constant, &[zero_idx], span);
        let i_sym = self.define_and_set(&i_name, true, span)?;

        let loop_start = self.current_instructions().len();
        self.load_symbol(&i_sym, span);
        self.load_symbol(&len_sym, span);
        self.emit(Opcode::LessThan, &[], span);
        let exit_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.load_symbol(&iter_sym, span);
        self.load_symbol(&i_sym, span);
        self.emit(Opcode::Index, &[], span);
        let elem_sym = self.define_and_set(&elem_name, false, span)?;

        self.load_symbol(&fn_sym, span);
        self.load_symbol(&elem_sym, span);
        self.emit(Opcode::Call, &[1], span);
        self.emit(Opcode::Pop, &[], span);

        self.load_symbol(&i_sym, span);
        let one_idx = self.add_constant(Value::Integer(Integer::I64(1)))?;
        self.emit(Opcode::Constant, &[one_idx], span);
        self.emit(Opcode::Add, &[], span);
        self.emit_set_symbol(&i_sym, span);
        self.emit(Opcode::Jump, &[loop_start], span);

        let loop_exit = self.current_instructions().len();
        self.replace_operand(exit_jump, loop_exit)?;

        let unit_idx = self.add_constant(Value::Unit)?;
        self.emit(Opcode::Constant, &[unit_idx], span);
        Ok(())
    }

    /// Compiles `vec.flat_map(f)`, applying `f` to each element (producing a
    /// vector), then concatenating all results into a single vector.
    fn compile_vector_flat_map(&mut self, mc: &MethodCallExpr) -> Result<()> {
        let span = mc.span;
        let id = self.for_loop_counter;
        self.for_loop_counter += 1;

        let fn_name = format!("__vfm_fn_{id}");
        let iter_name = format!("__vfm_iter_{id}");
        let len_name = format!("__vfm_len_{id}");
        let result_name = format!("__vfm_result_{id}");
        let i_name = format!("__vfm_i_{id}");
        let elem_name = format!("__vfm_elem_{id}");

        self.compile_expression(&mc.arguments[0])?;
        let fn_sym = self.define_and_set(&fn_name, false, span)?;

        self.compile_expression(&mc.object)?;
        let iter_sym = self.define_and_set(&iter_name, false, span)?;

        let len_builtin = self.resolve_or_error("Vector::len", span)?;
        self.load_symbol(&len_builtin, span);
        self.load_symbol(&iter_sym, span);
        self.emit(Opcode::Call, &[1], span);
        let len_sym = self.define_and_set(&len_name, false, span)?;

        self.emit(Opcode::Vector, &[0], span);
        let result_sym = self.define_and_set(&result_name, true, span)?;

        let zero_idx = self.add_constant(Value::Integer(Integer::I64(0)))?;
        self.emit(Opcode::Constant, &[zero_idx], span);
        let i_sym = self.define_and_set(&i_name, true, span)?;

        let loop_start = self.current_instructions().len();
        self.load_symbol(&i_sym, span);
        self.load_symbol(&len_sym, span);
        self.emit(Opcode::LessThan, &[], span);
        let exit_jump = self.emit(Opcode::CondJump, &[Self::JUMP], span);

        self.load_symbol(&iter_sym, span);
        self.load_symbol(&i_sym, span);
        self.emit(Opcode::Index, &[], span);
        let elem_sym = self.define_and_set(&elem_name, false, span)?;

        let chain_builtin = self.resolve_or_error("Vector::chain", span)?;
        self.load_symbol(&chain_builtin, span);
        self.load_symbol(&result_sym, span);
        self.load_symbol(&fn_sym, span);
        self.load_symbol(&elem_sym, span);
        self.emit(Opcode::Call, &[1], span);
        self.emit(Opcode::Call, &[2], span);
        self.emit_set_symbol(&result_sym, span);

        self.load_symbol(&i_sym, span);
        let one_idx = self.add_constant(Value::Integer(Integer::I64(1)))?;
        self.emit(Opcode::Constant, &[one_idx], span);
        self.emit(Opcode::Add, &[], span);
        self.emit_set_symbol(&i_sym, span);
        self.emit(Opcode::Jump, &[loop_start], span);

        let loop_exit = self.current_instructions().len();
        self.replace_operand(exit_jump, loop_exit)?;
        self.load_symbol(&result_sym, span);
        Ok(())
    }

    /// Compiles the try operator (`expr?`).
    ///
    /// Desugars to an inline match:
    /// - `Option<T>`: `Some(val) => val`, `None => return None`
    /// - `Result<T, E>`: `Ok(val) => val`, `Err(e) => return Err(e)`
    fn compile_try(&mut self, try_expr: &TryExpr) -> Result<()> {
        let span = try_expr.span;
        let is_option = try_expr.kind == TryKind::Option;

        let success_tag: usize = 0;
        let type_index: usize = if is_option { 0 } else { 1 };
        let none_or_err = (type_index << 8) | 1;

        self.compile_expression(&try_expr.expr)?;

        let match_tag_pos = self.emit(Opcode::MatchTag, &[success_tag, Self::JUMP], span);

        self.emit(Opcode::GetField, &[0], span);
        let jump_to_end = self.emit(Opcode::Jump, &[Self::JUMP], span);

        let fail_arm = self.current_instructions().len();
        self.replace_match_tag_target(match_tag_pos, fail_arm)?;

        if is_option {
            self.emit(Opcode::Pop, &[], span);
            self.emit(Opcode::Construct, &[none_or_err, 0], span);
        }
        // For Result, the Err variant is already on the stack.
        self.emit(Opcode::ReturnValue, &[], span);

        let end = self.current_instructions().len();
        self.replace_operand(jump_to_end, end)?;
        Ok(())
    }

    /// Resolves the fully-qualified builtin name for a method call.
    fn resolve_method_name(&mut self, mc: &MethodCallExpr) -> Option<String> {
        if let Some(ref receiver) = mc.receiver {
            let candidate = format!("{receiver}::{}", mc.method);
            if self.symbols_table.resolve_symbol(&candidate).is_some() {
                return Some(candidate);
            }
        }
        self.type_registry
            .iter()
            .map(|td| match td {
                TypeDef::Struct { name, .. } | TypeDef::Enum { name, .. } => name.as_str(),
            })
            .chain(BUILTIN_METHOD_PREFIXES.iter().copied())
            .find_map(|type_name| {
                let candidate = format!("{type_name}::{}", mc.method);
                self.symbols_table
                    .resolve_symbol(&candidate)
                    .map(|_| candidate)
            })
    }

    /// Resolves a symbol by name, returning an error if undefined.
    fn resolve_or_error(&mut self, name: &str, span: Span) -> Result<Symbol> {
        self.symbols_table.resolve_symbol(name).ok_or_else(|| {
            CompileErrorKind::UndefinedVariable {
                name: name.to_string(),
            }
            .at(span)
            .into()
        })
    }

    /// Attaches a span to a compile error that lacks one.
    fn attach_span(&self, err: Error, span: Span) -> Error {
        match err {
            Error::Compile(ce) if ce.span.is_none() => CompileError {
                kind: ce.kind,
                span: Some(span),
            }
            .into(),
            other => other,
        }
    }

    /// Adds a constant value to the constant pool and returns its index.
    ///
    /// # Errors
    ///
    /// Returns `CompileError::ConstantPoolOverflow` if adding this constant
    /// would exceed the maximum constant pool size.
    fn add_constant(&mut self, val: Value) -> Result<usize> {
        let index = self.constants.len();
        if index > MAX_CONSTANT_POOL_SIZE {
            return Err(CompileError::new(CompileErrorKind::ConstantPoolOverflow {
                max: MAX_CONSTANT_POOL_SIZE,
                attempted: index,
            })
            .into());
        }
        self.constants.push(val);
        Ok(index)
    }

    /// Returns a reference to the current scope's instruction stream.
    fn current_instructions(&self) -> &Instructions {
        &self.scopes[self.scope_index].instructions
    }

    /// Enters a new compilation scope for a function body.
    fn compile_fn_body<'a>(
        &mut self,
        name: Option<&str>,
        param_names: impl Iterator<Item = &'a str>,
        num_params: usize,
        body: &BlockStmt,
        span: Span,
    ) -> Result<()> {
        self.enter_scope();
        if let Some(name) = name {
            self.symbols_table.define_function_name(name);
        }
        for param in param_names {
            if let Err(e) = self.symbols_table.define_symbol(param, false) {
                return Err(self.attach_span(e, span));
            }
        }
        self.compile_block_statement(body)?;
        if self.last_instruction_is(Opcode::Pop) {
            self.replace_last_pop_with_return_value();
        }
        if !self.last_instruction_is(Opcode::ReturnValue) {
            self.emit(Opcode::Return, &[], span);
        }
        let free_vars = self.symbols_table.free_vars().to_vec();
        let num_free = free_vars.len();
        let num_locals = self.symbols_table.max_definitions();
        let (instructions, inner_source_map) = self.leave_scope()?;

        for sym in &free_vars {
            self.load_symbol(sym, span);
        }
        let compiled_fn = Value::CompiledFn(CompiledFn {
            instructions: Rc::from(instructions.as_bytes()),
            num_locals,
            num_parameters: num_params,
            source_map: inner_source_map,
        });
        let index = self.add_constant(compiled_fn)?;
        self.emit(Opcode::Closure, &[index, num_free], span);
        Ok(())
    }

    /// Compiles an `impl` block by emitting each method as a named function
    /// binding. Methods with a `self` parameter have it compiled as the first
    /// regular parameter.
    fn compile_impl_block(&mut self, impl_block: &ImplBlock) -> Result<()> {
        let type_name = match &impl_block.self_type {
            TypeExpr::Named(n) => &n.name,
            TypeExpr::Generic(name, _, _) => name,
            _ => return Ok(()),
        };
        for method in &impl_block.methods {
            let span = method.span;
            let qualified_name = format!("{}::{}", type_name, method.name);
            self.compile_fn_body(
                Some(&qualified_name),
                method.param_names(),
                method.params.len(),
                &method.body,
                span,
            )?;
            self.define_and_set(&qualified_name, false, span)?;
        }
        Ok(())
    }

    fn enter_scope(&mut self) {
        self.scopes.push(CompilationScope::new());
        self.scope_index += 1;
        let outer = std::mem::take(&mut self.symbols_table);
        self.symbols_table = SymbolsTable::new_enclosed(outer);
    }

    /// Leaves the current compilation scope, returning its instructions
    /// and source map.
    ///
    /// # Errors
    ///
    /// Returns `CompileError::ScopeUnderflow` if the scope stack is empty
    /// or the enclosed symbol table has no outer table to restore.
    fn leave_scope(&mut self) -> Result<(Instructions, SourceMap)> {
        if self.scope_index == 0 {
            return Err(CompileError::new(CompileErrorKind::ScopeUnderflow).into());
        }
        let scope = self
            .scopes
            .pop()
            .ok_or(CompileError::new(CompileErrorKind::ScopeUnderflow))?;
        self.scope_index -= 1;
        let current = std::mem::take(&mut self.symbols_table);
        self.symbols_table = current
            .take_outer()
            .ok_or(CompileError::new(CompileErrorKind::ScopeUnderflow))?;
        Ok((scope.instructions, scope.source_map))
    }

    /// Defines a symbol and emits the corresponding store instruction.
    fn define_and_set(&mut self, name: &str, mutable: bool, span: Span) -> Result<Symbol> {
        let symbol = match self.symbols_table.define_symbol(name, mutable) {
            Ok(s) => s.clone(),
            Err(e) => return Err(self.attach_span(e, span)),
        };
        self.emit_set_symbol(&symbol, span);
        Ok(symbol)
    }

    /// Emits the appropriate store instruction for a resolved symbol.
    fn emit_set_symbol(&mut self, symbol: &Symbol, span: Span) {
        match symbol.scope {
            SymbolScope::Global => self.emit(Opcode::SetGlobal, &[symbol.index], span),
            SymbolScope::Local => self.emit(Opcode::SetLocal, &[symbol.index], span),
            SymbolScope::Builtin | SymbolScope::Free | SymbolScope::Function => {
                unreachable!("define_symbol never produces this scope")
            }
        };
    }

    /// Emits the appropriate load instruction for a resolved symbol.
    fn load_symbol(&mut self, symbol: &Symbol, span: Span) {
        match symbol.scope {
            SymbolScope::Global => self.emit(Opcode::GetGlobal, &[symbol.index], span),
            SymbolScope::Local => self.emit(Opcode::GetLocal, &[symbol.index], span),
            SymbolScope::Builtin => self.emit(Opcode::GetBuiltin, &[symbol.index], span),
            SymbolScope::Free => self.emit(Opcode::GetFree, &[symbol.index], span),
            SymbolScope::Function => self.emit(Opcode::CurrentClosure, &[], span),
        };
    }

    /// Emits a bytecode instruction with the given opcode and operands.
    fn emit(&mut self, opcode: Opcode, operands: &[usize], span: Span) -> usize {
        let instruction = encode(opcode, operands);
        let pos = self.add_instruction(&instruction);
        self.scopes[self.scope_index].source_map.add(pos, span);
        self.set_last_instruction(opcode, pos);
        pos
    }

    /// Appends instruction bytes to the current scope's instruction stream.
    ///
    /// Returns the position where the instruction was inserted.
    fn add_instruction(&mut self, instruction: &[u8]) -> usize {
        let scope = &mut self.scopes[self.scope_index];
        let pos = scope.instructions.len();
        scope.instructions.extend_from_bytes(instruction);
        pos
    }

    /// Updates the last/previous instruction tracking in the current scope.
    fn set_last_instruction(&mut self, opcode: Opcode, position: usize) {
        let scope = &mut self.scopes[self.scope_index];
        scope.previous_instruction = scope.last_instruction;
        scope.last_instruction = Some(Instruction { opcode, position });
    }

    /// Returns `true` if the last emitted instruction matches the given opcode.
    fn last_instruction_is(&self, opcode: Opcode) -> bool {
        self.scopes[self.scope_index]
            .last_instruction
            .is_some_and(|last| last.opcode == opcode)
    }

    /// Removes the last `OpPop` instruction from the stream.
    ///
    /// This is used when compiling block expressions within conditionals:
    /// the block's expression statements emit `OpPop`, but conditionals
    /// need the value to remain on the stack.
    fn remove_last_pop(&mut self) {
        let scope = &mut self.scopes[self.scope_index];
        if let Some(last) = scope.last_instruction {
            scope.instructions.truncate(last.position);
            scope.last_instruction = scope.previous_instruction;
        }
    }

    /// Replaces the last `OpPop` with `OpReturnValue`.
    ///
    /// Used at the end of function bodies to convert the final expression
    /// statement's implicit pop into an explicit return.
    fn replace_last_pop_with_return_value(&mut self) {
        let scope = &mut self.scopes[self.scope_index];
        if let Some(last) = scope.last_instruction {
            let new_inst = encode(Opcode::ReturnValue, &[]);
            scope.instructions.replace_bytes(last.position, &new_inst);
            scope.last_instruction = Some(Instruction {
                opcode: Opcode::ReturnValue,
                position: last.position,
            });
        }
    }

    /// Replaces the operand of an instruction at the given position.
    fn replace_operand(&mut self, op_pos: usize, operand: usize) -> Result<()> {
        let scope = &mut self.scopes[self.scope_index];
        let byte = scope.instructions.as_bytes()[op_pos];
        let op =
            Opcode::from_byte(byte).ok_or(CompileError::new(CompileErrorKind::InvalidOpcode {
                opcode: byte,
                position: op_pos,
            }))?;
        let new_inst = encode(op, &[operand]);
        scope.instructions.replace_bytes(op_pos, &new_inst);
        Ok(())
    }

    /// Dispatches a builtin macro call to the appropriate expansion routine.
    fn compile_macro_call(&mut self, mc: &MacroCallExpr) -> Result<()> {
        let span = mc.span;
        match mc.name.as_str() {
            "format" => self.compile_format_macro(&mc.arguments, span),
            "print" => self.compile_print_macro(&mc.arguments, false, span),
            "println" => self.compile_print_macro(&mc.arguments, true, span),
            "assert" => self.compile_assert_macro(&mc.arguments, span),
            "assert_eq" => self.compile_assert_eq_macro(&mc.arguments, span),
            "panic" => self.compile_panic_macro(&mc.arguments, span),
            "todo" => self.emit_builtin_call(
                "__panic",
                &[Value::Str("not yet implemented".to_string())],
                span,
            ),
            "unimplemented" => self.emit_builtin_call(
                "__panic",
                &[Value::Str("not implemented".to_string())],
                span,
            ),
            _ => Err(CompileErrorKind::UnknownMacro {
                name: mc.name.clone(),
            }
            .at(span)
            .into()),
        }
    }

    /// Compiles `format!(fmt, args...)` into a string concatenation sequence.
    fn compile_format_macro(&mut self, args: &[Expr], span: Span) -> Result<()> {
        if args.is_empty() {
            let idx = self.add_constant(Value::Str(String::new()))?;
            self.emit(Opcode::Constant, &[idx], span);
            return Ok(());
        }

        let fmt = match &args[0] {
            Expr::Str(s) => unescape_string(&s.value),
            _ => {
                return Err(CompileErrorKind::MacroExpectsFormatString {
                    macro_name: "format".to_string(),
                }
                .at(span)
                .into());
            }
        };

        let segments = parse_format_string(&fmt);
        let placeholder_count = segments
            .iter()
            .filter(|s| matches!(s, FmtSegment::Arg))
            .count();
        let value_args = &args[1..];
        if placeholder_count != value_args.len() {
            return Err(CompileErrorKind::FormatArgCountMismatch {
                placeholders: placeholder_count,
                arguments: value_args.len(),
            }
            .at(span)
            .into());
        }

        let mut arg_idx = 0;
        let mut pieces = 0usize;

        for segment in &segments {
            match segment {
                FmtSegment::Literal(text) => {
                    let idx = self.add_constant(Value::Str(text.clone()))?;
                    self.emit(Opcode::Constant, &[idx], span);
                    pieces += 1;
                }
                FmtSegment::Arg => {
                    self.emit_builtin_call_expr("__to_string", &value_args[arg_idx], span)?;
                    arg_idx += 1;
                    pieces += 1;
                }
                FmtSegment::Capture(name) => {
                    let ident_expr = Expr::Ident(Ident {
                        value: name.clone(),
                        span,
                    });
                    self.emit_builtin_call_expr("__to_string", &ident_expr, span)?;
                    pieces += 1;
                }
            }
            if pieces > 1 {
                self.emit(Opcode::Add, &[], span);
            }
        }

        if pieces == 0 {
            let idx = self.add_constant(Value::Str(String::new()))?;
            self.emit(Opcode::Constant, &[idx], span);
        }

        Ok(())
    }

    /// Compiles `print!(fmt, args...)` or `println!(fmt, args...)`.
    fn compile_print_macro(&mut self, args: &[Expr], newline: bool, span: Span) -> Result<()> {
        let macro_name = if newline { "println" } else { "print" };

        if args.is_empty() {
            if newline {
                return self.emit_builtin_call(
                    "__print_str_ln",
                    &[Value::Str(String::new())],
                    span,
                );
            }
            return self.emit_builtin_call("__print_str", &[Value::Str(String::new())], span);
        }
        let fmt = match &args[0] {
            Expr::Str(s) => unescape_string(&s.value),
            _ => {
                return Err(CompileErrorKind::MacroExpectsFormatString {
                    macro_name: macro_name.to_string(),
                }
                .at(span)
                .into());
            }
        };
        let segments = parse_format_string(&fmt);
        let placeholder_count = segments
            .iter()
            .filter(|s| matches!(s, FmtSegment::Arg))
            .count();
        let value_args = &args[1..];
        if placeholder_count != value_args.len() {
            return Err(CompileErrorKind::FormatArgCountMismatch {
                placeholders: placeholder_count,
                arguments: value_args.len(),
            }
            .at(span)
            .into());
        }

        // Each builtin call pushes a unit result. We pop all intermediate
        // results and keep only the final call's result as the expression value.
        let mut arg_idx = 0;
        let mut emitted_calls = 0usize;

        for segment in &segments {
            if emitted_calls > 0 {
                self.emit(Opcode::Pop, &[], span);
            }
            match segment {
                FmtSegment::Literal(text) => {
                    self.emit_builtin_call("__print_str", &[Value::Str(text.clone())], span)?;
                    emitted_calls += 1;
                }
                FmtSegment::Arg => {
                    self.emit_builtin_call_expr("__to_string", &value_args[arg_idx], span)?;
                    self.emit_builtin_call_stack("__print_str", span)?;
                    arg_idx += 1;
                    emitted_calls += 1;
                }
                FmtSegment::Capture(name) => {
                    let ident_expr = Expr::Ident(Ident {
                        value: name.clone(),
                        span,
                    });
                    self.emit_builtin_call_expr("__to_string", &ident_expr, span)?;
                    self.emit_builtin_call_stack("__print_str", span)?;
                    emitted_calls += 1;
                }
            }
        }
        if newline {
            if emitted_calls > 0 {
                self.emit(Opcode::Pop, &[], span);
            }
            self.emit_builtin_call("__print_str_ln", &[Value::Str(String::new())], span)?;
        }
        Ok(())
    }

    /// Compiles `assert!(condition)` or `assert!(condition, message)`.
    fn compile_assert_macro(&mut self, args: &[Expr], span: Span) -> Result<()> {
        if args.is_empty() || args.len() > 2 {
            return Err(CompileErrorKind::FormatArgCountMismatch {
                placeholders: 1,
                arguments: args.len(),
            }
            .at(span)
            .into());
        }

        self.compile_expression(&args[0])?;
        let cond_jump_pos = self.emit(Opcode::CondJump, &[Self::JUMP], span);
        let skip_pos = self.emit(Opcode::Jump, &[Self::JUMP], span);

        let panic_start = self.current_instructions().len();
        self.replace_operand(cond_jump_pos, panic_start)?;

        if args.len() == 2 {
            self.compile_expression(&args[1])?;
            self.emit_builtin_call_stack("__panic", span)?;
        } else {
            self.emit_builtin_call(
                "__panic",
                &[Value::Str("assertion failed".to_string())],
                span,
            )?;
        }
        self.emit(Opcode::Pop, &[], span);

        let after = self.current_instructions().len();
        self.replace_operand(skip_pos, after)?;
        self.emit(Opcode::Unit, &[], span);
        Ok(())
    }

    /// Compiles `assert_eq!(left, right)`.
    fn compile_assert_eq_macro(&mut self, args: &[Expr], span: Span) -> Result<()> {
        if args.len() != 2 {
            return Err(CompileErrorKind::FormatArgCountMismatch {
                placeholders: 2,
                arguments: args.len(),
            }
            .at(span)
            .into());
        }

        self.compile_expression(&args[0])?;
        self.compile_expression(&args[1])?;
        self.emit(Opcode::Equal, &[], span);

        let cond_jump_pos = self.emit(Opcode::CondJump, &[Self::JUMP], span);
        let skip_pos = self.emit(Opcode::Jump, &[Self::JUMP], span);

        let panic_start = self.current_instructions().len();
        self.replace_operand(cond_jump_pos, panic_start)?;
        self.emit_builtin_call(
            "__panic",
            &[Value::Str("assertion `left == right` failed".to_string())],
            span,
        )?;
        self.emit(Opcode::Pop, &[], span);

        let after = self.current_instructions().len();
        self.replace_operand(skip_pos, after)?;
        self.emit(Opcode::Unit, &[], span);
        Ok(())
    }

    /// Compiles `panic!()`, `panic!("message")`, or `panic!("fmt {}", args...)`.
    ///
    /// With no arguments, emits `__panic("explicit panic")`. With a plain
    /// string literal, emits `__panic(literal)`. With a format string and
    /// interpolation arguments, builds the message by concatenating segments
    /// via `__to_string` and `__str_concat`, then emits `__panic(result)`.
    fn compile_panic_macro(&mut self, args: &[Expr], span: Span) -> Result<()> {
        if args.is_empty() {
            return self.emit_builtin_call(
                "__panic",
                &[Value::Str("explicit panic".to_string())],
                span,
            );
        }

        let fmt = match &args[0] {
            Expr::Str(s) => unescape_string(&s.value),
            _ => {
                return Err(CompileErrorKind::MacroExpectsFormatString {
                    macro_name: "panic".to_string(),
                }
                .at(span)
                .into());
            }
        };

        let segments = parse_format_string(&fmt);
        let placeholder_count = segments
            .iter()
            .filter(|s| matches!(s, FmtSegment::Arg))
            .count();
        let value_args = &args[1..];
        if placeholder_count != value_args.len() {
            return Err(CompileErrorKind::FormatArgCountMismatch {
                placeholders: placeholder_count,
                arguments: value_args.len(),
            }
            .at(span)
            .into());
        }

        if placeholder_count == 0 {
            return self.emit_builtin_call("__panic", &[Value::Str(fmt)], span);
        }

        let mut arg_idx = 0;
        let mut first = true;

        for segment in &segments {
            let segment_str = match segment {
                FmtSegment::Literal(text) => {
                    let idx = self.add_constant(Value::Str(text.clone()))?;
                    self.emit(Opcode::Constant, &[idx], span);
                    true
                }
                FmtSegment::Arg => {
                    self.emit_builtin_call_expr("__to_string", &value_args[arg_idx], span)?;
                    arg_idx += 1;
                    true
                }
                FmtSegment::Capture(name) => {
                    let ident_expr = Expr::Ident(Ident {
                        value: name.clone(),
                        span,
                    });
                    self.emit_builtin_call_expr("__to_string", &ident_expr, span)?;
                    true
                }
            };

            if segment_str && !first {
                let rhs_tmp = format!("__panic_rhs_{}", self.current_instructions().len());
                let rhs_sym = self.define_and_set(&rhs_tmp, false, span)?;
                let lhs_tmp = format!("__panic_lhs_{}", self.current_instructions().len());
                let lhs_sym = self.define_and_set(&lhs_tmp, false, span)?;
                let concat_idx = registry::resolve_builtin_index("__str_concat");
                self.emit(Opcode::GetBuiltin, &[concat_idx], span);
                self.load_symbol(&lhs_sym, span);
                self.load_symbol(&rhs_sym, span);
                self.emit(Opcode::Call, &[2], span);
            }
            if segment_str {
                first = false;
            }
        }

        self.emit_builtin_call_stack("__panic", span)?;
        Ok(())
    }

    /// Emits a call to a builtin function with constant arguments.
    fn emit_builtin_call(&mut self, name: &str, const_args: &[Value], span: Span) -> Result<()> {
        let builtin_idx = registry::resolve_builtin_index(name);
        self.emit(Opcode::GetBuiltin, &[builtin_idx], span);
        for arg in const_args {
            let idx = self.add_constant(arg.clone())?;
            self.emit(Opcode::Constant, &[idx], span);
        }
        self.emit(Opcode::Call, &[const_args.len()], span);
        Ok(())
    }

    /// Emits a call to a builtin function where the single argument is an
    /// expression to be compiled (pushed onto the stack).
    fn emit_builtin_call_expr(&mut self, name: &str, arg: &Expr, span: Span) -> Result<()> {
        let builtin_idx = registry::resolve_builtin_index(name);
        self.emit(Opcode::GetBuiltin, &[builtin_idx], span);
        self.compile_expression(arg)?;
        self.emit(Opcode::Call, &[1], span);
        Ok(())
    }

    /// Emits a call to a builtin function where the single argument is
    /// already on the stack (from a prior call's return value).
    fn emit_builtin_call_stack(&mut self, name: &str, span: Span) -> Result<()> {
        let temp_name = format!("__macro_tmp_{}", self.current_instructions().len());
        let symbol = self.define_and_set(&temp_name, false, span)?;
        let builtin_idx = registry::resolve_builtin_index(name);
        self.emit(Opcode::GetBuiltin, &[builtin_idx], span);
        self.load_symbol(&symbol, span);
        self.emit(Opcode::Call, &[1], span);
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn constant_pool_overflow() {
        let mut compiler = Compiler::new();
        for i in 0..=MAX_CONSTANT_POOL_SIZE as i64 {
            let result = compiler.add_constant(Value::Integer(Integer::I64(i)));
            assert!(result.is_ok(), "should succeed for index {i}");
        }
        let result = compiler.add_constant(Value::Integer(Integer::I64(999)));
        assert!(
            result.is_err(),
            "should fail when exceeding MAX_CONSTANT_POOL_SIZE"
        );
        match result.unwrap_err() {
            Error::Compile(CompileError {
                kind: CompileErrorKind::ConstantPoolOverflow { max, attempted },
                ..
            }) => {
                assert_eq!(max, MAX_CONSTANT_POOL_SIZE);
                assert_eq!(attempted, MAX_CONSTANT_POOL_SIZE + 1);
            }
            other => panic!("expected ConstantPoolOverflow, got {:?}", other),
        }
    }

    #[test]
    fn unsupported_prefix_operator() {
        use maat_ast::{ExprStmt, NumKind, Number, PrefixExpr, Radix};

        let expr = Expr::Prefix(PrefixExpr {
            operator: "~".to_string(),
            operand: Box::new(Expr::Number(Number {
                kind: NumKind::I64,
                value: 5,
                radix: Radix::Dec,
                span: Span::ZERO,
            })),
            span: Span::ZERO,
        });
        let program = Program {
            statements: vec![Stmt::Expr(ExprStmt {
                value: expr,
                span: Span::ZERO,
            })],
        };
        let mut compiler = Compiler::new();
        let result = compiler.compile(&Node::Program(program));
        assert!(
            result.is_err(),
            "should fail on unsupported prefix operator"
        );
        match result.unwrap_err() {
            Error::Compile(CompileError {
                kind: CompileErrorKind::UnsupportedOperator { operator, context },
                ..
            }) => {
                assert_eq!(operator, "~");
                assert_eq!(context, "prefix expression");
            }
            other => panic!("expected UnsupportedOperator, got {:?}", other),
        }
    }

    #[test]
    fn scopes() {
        let mut compiler = Compiler::new();
        assert_eq!(compiler.scope_index, 0);
        compiler.emit(Opcode::Mul, &[], Span::ZERO);
        compiler.enter_scope();
        assert_eq!(compiler.scope_index, 1);
        compiler.emit(Opcode::Sub, &[], Span::ZERO);
        assert_eq!(compiler.scopes[compiler.scope_index].instructions.len(), 1);
        assert_eq!(
            compiler.scopes[compiler.scope_index]
                .last_instruction
                .unwrap()
                .opcode,
            Opcode::Sub
        );
        let (instructions, _source_map) = compiler.leave_scope().expect("should leave scope");
        assert_eq!(compiler.scope_index, 0);
        assert_eq!(instructions.len(), 1);
        assert_eq!(
            compiler.scopes[compiler.scope_index]
                .last_instruction
                .unwrap()
                .opcode,
            Opcode::Mul
        );
    }
}
