Skip to main content

harn_vm/
compiler.rs

1use harn_lexer::StringSegment;
2use harn_parser::{BindingPattern, Node, SNode, TypedParam};
3
4use crate::chunk::{Chunk, CompiledFunction, Constant, Op};
5
6/// Compile error.
7#[derive(Debug)]
8pub struct CompileError {
9    pub message: String,
10    pub line: u32,
11}
12
13impl std::fmt::Display for CompileError {
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        write!(f, "Compile error at line {}: {}", self.line, self.message)
16    }
17}
18
19impl std::error::Error for CompileError {}
20
21/// Tracks loop context for break/continue compilation.
22struct LoopContext {
23    /// Offset of the loop start (for continue).
24    start_offset: usize,
25    /// Positions of break jumps that need patching to the loop end.
26    break_patches: Vec<usize>,
27    /// True if this is a for-in loop (has an iterator to clean up on break).
28    has_iterator: bool,
29    /// Number of exception handlers active at loop entry.
30    handler_depth: usize,
31    /// Number of pending finally bodies at loop entry.
32    finally_depth: usize,
33}
34
35/// Compiles an AST into bytecode.
36pub struct Compiler {
37    chunk: Chunk,
38    line: u32,
39    column: u32,
40    /// Track enum type names so PropertyAccess on them can produce EnumVariant.
41    enum_names: std::collections::HashSet<String>,
42    /// Track interface names → method names for runtime enforcement.
43    interface_methods: std::collections::HashMap<String, Vec<String>>,
44    /// Stack of active loop contexts for break/continue.
45    loop_stack: Vec<LoopContext>,
46    /// Current depth of exception handlers (for cleanup on break/continue).
47    handler_depth: usize,
48    /// Stack of pending finally bodies for return/break/continue handling.
49    finally_bodies: Vec<Vec<SNode>>,
50    /// Counter for unique temp variable names.
51    temp_counter: usize,
52}
53
54impl Compiler {
55    pub fn new() -> Self {
56        Self {
57            chunk: Chunk::new(),
58            line: 1,
59            column: 1,
60            enum_names: std::collections::HashSet::new(),
61            interface_methods: std::collections::HashMap::new(),
62            loop_stack: Vec::new(),
63            handler_depth: 0,
64            finally_bodies: Vec::new(),
65            temp_counter: 0,
66        }
67    }
68
69    /// Compile a program (list of top-level nodes) into a Chunk.
70    /// Finds the entry pipeline and compiles its body, including inherited bodies.
71    pub fn compile(mut self, program: &[SNode]) -> Result<Chunk, CompileError> {
72        // Pre-scan the entire program for enum declarations (including inside pipelines)
73        // so we can recognize EnumName.Variant as enum construction.
74        Self::collect_enum_names(program, &mut self.enum_names);
75        // Built-in Result enum is always available
76        self.enum_names.insert("Result".to_string());
77        Self::collect_interface_methods(program, &mut self.interface_methods);
78
79        // Compile all top-level non-pipeline declarations first (fn, enum, etc.)
80        for sn in program {
81            match &sn.node {
82                Node::ImportDecl { .. } | Node::SelectiveImport { .. } => {
83                    self.compile_node(sn)?;
84                }
85                _ => {}
86            }
87        }
88
89        // Find entry pipeline
90        let main = program
91            .iter()
92            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == "default"))
93            .or_else(|| {
94                program
95                    .iter()
96                    .find(|sn| matches!(&sn.node, Node::Pipeline { .. }))
97            });
98
99        if let Some(sn) = main {
100            if let Node::Pipeline { body, extends, .. } = &sn.node {
101                // If this pipeline extends another, compile the parent chain first
102                if let Some(parent_name) = extends {
103                    self.compile_parent_pipeline(program, parent_name)?;
104                }
105                self.compile_block(body)?;
106            }
107        }
108
109        self.chunk.emit(Op::Nil, self.line);
110        self.chunk.emit(Op::Return, self.line);
111        Ok(self.chunk)
112    }
113
114    /// Compile a specific named pipeline (for test runners).
115    pub fn compile_named(
116        mut self,
117        program: &[SNode],
118        pipeline_name: &str,
119    ) -> Result<Chunk, CompileError> {
120        Self::collect_enum_names(program, &mut self.enum_names);
121        Self::collect_interface_methods(program, &mut self.interface_methods);
122
123        for sn in program {
124            if matches!(
125                &sn.node,
126                Node::ImportDecl { .. } | Node::SelectiveImport { .. }
127            ) {
128                self.compile_node(sn)?;
129            }
130        }
131
132        let target = program
133            .iter()
134            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == pipeline_name));
135
136        if let Some(sn) = target {
137            if let Node::Pipeline { body, extends, .. } = &sn.node {
138                if let Some(parent_name) = extends {
139                    self.compile_parent_pipeline(program, parent_name)?;
140                }
141                self.compile_block(body)?;
142            }
143        }
144
145        self.chunk.emit(Op::Nil, self.line);
146        self.chunk.emit(Op::Return, self.line);
147        Ok(self.chunk)
148    }
149
150    /// Recursively compile parent pipeline bodies (for extends).
151    fn compile_parent_pipeline(
152        &mut self,
153        program: &[SNode],
154        parent_name: &str,
155    ) -> Result<(), CompileError> {
156        let parent = program
157            .iter()
158            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == parent_name));
159        if let Some(sn) = parent {
160            if let Node::Pipeline { body, extends, .. } = &sn.node {
161                // Recurse if this parent also extends another
162                if let Some(grandparent) = extends {
163                    self.compile_parent_pipeline(program, grandparent)?;
164                }
165                // Compile parent body - pop all statement values
166                for stmt in body {
167                    self.compile_node(stmt)?;
168                    if Self::produces_value(&stmt.node) {
169                        self.chunk.emit(Op::Pop, self.line);
170                    }
171                }
172            }
173        }
174        Ok(())
175    }
176
177    /// Emit bytecode preamble for default parameter values.
178    /// For each param with a default at index i, emits:
179    ///   GetArgc; PushInt (i+1); GreaterEqual; JumpIfTrue <skip>;
180    ///   [compile default expr]; DefLet param_name; <skip>:
181    fn emit_default_preamble(&mut self, params: &[TypedParam]) -> Result<(), CompileError> {
182        for (i, param) in params.iter().enumerate() {
183            if let Some(default_expr) = &param.default_value {
184                self.chunk.emit(Op::GetArgc, self.line);
185                let threshold_idx = self.chunk.add_constant(Constant::Int((i + 1) as i64));
186                self.chunk.emit_u16(Op::Constant, threshold_idx, self.line);
187                // argc >= (i+1) means arg was provided
188                self.chunk.emit(Op::GreaterEqual, self.line);
189                let skip_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
190                // Pop the boolean from JumpIfTrue (it doesn't pop)
191                self.chunk.emit(Op::Pop, self.line);
192                // Compile the default expression
193                self.compile_node(default_expr)?;
194                let name_idx = self
195                    .chunk
196                    .add_constant(Constant::String(param.name.clone()));
197                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
198                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
199                self.chunk.patch_jump(skip_jump);
200                // Pop the boolean left by JumpIfTrue on the true path
201                self.chunk.emit(Op::Pop, self.line);
202                self.chunk.patch_jump(end_jump);
203            }
204        }
205        Ok(())
206    }
207
208    /// Emit runtime type checks for parameters with type annotations.
209    /// For each param with a type annotation, emits CheckType(var_name, type_name)
210    /// or calls __assert_shape for shape types.
211    fn emit_type_checks(&mut self, params: &[TypedParam]) {
212        for param in params {
213            if let Some(type_expr) = &param.type_expr {
214                // Handle shape types via __assert_shape builtin call
215                if let harn_parser::TypeExpr::Shape(fields) = type_expr {
216                    let spec = Self::shape_to_spec_string(fields);
217                    // Emit: __assert_shape(param_value, param_name, spec)
218                    let fn_idx = self
219                        .chunk
220                        .add_constant(Constant::String("__assert_shape".into()));
221                    self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
222                    let var_idx = self
223                        .chunk
224                        .add_constant(Constant::String(param.name.clone()));
225                    self.chunk.emit_u16(Op::GetVar, var_idx, self.line);
226                    let name_idx = self
227                        .chunk
228                        .add_constant(Constant::String(param.name.clone()));
229                    self.chunk.emit_u16(Op::Constant, name_idx, self.line);
230                    let spec_idx = self.chunk.add_constant(Constant::String(spec));
231                    self.chunk.emit_u16(Op::Constant, spec_idx, self.line);
232                    self.chunk.emit_u8(Op::Call, 3, self.line);
233                    self.chunk.emit(Op::Pop, self.line);
234                    continue;
235                }
236
237                // Check if this is an interface type — emit __assert_interface
238                if let harn_parser::TypeExpr::Named(name) = type_expr {
239                    if let Some(methods) = self.interface_methods.get(name) {
240                        let fn_idx = self
241                            .chunk
242                            .add_constant(Constant::String("__assert_interface".into()));
243                        self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
244                        let var_idx = self
245                            .chunk
246                            .add_constant(Constant::String(param.name.clone()));
247                        self.chunk.emit_u16(Op::GetVar, var_idx, self.line);
248                        let name_idx = self
249                            .chunk
250                            .add_constant(Constant::String(param.name.clone()));
251                        self.chunk.emit_u16(Op::Constant, name_idx, self.line);
252                        let iface_idx = self.chunk.add_constant(Constant::String(name.clone()));
253                        self.chunk.emit_u16(Op::Constant, iface_idx, self.line);
254                        let methods_str = methods.join(",");
255                        let methods_idx = self.chunk.add_constant(Constant::String(methods_str));
256                        self.chunk.emit_u16(Op::Constant, methods_idx, self.line);
257                        self.chunk.emit_u8(Op::Call, 4, self.line);
258                        self.chunk.emit(Op::Pop, self.line);
259                        continue;
260                    }
261                }
262
263                let type_name = Self::type_expr_to_runtime_name(type_expr);
264                if let Some(type_name) = type_name {
265                    let var_idx = self
266                        .chunk
267                        .add_constant(Constant::String(param.name.clone()));
268                    let type_idx = self.chunk.add_constant(Constant::String(type_name));
269                    self.chunk.emit_u16(Op::CheckType, var_idx, self.line);
270                    // Emit the type name index as two extra bytes
271                    let hi = (type_idx >> 8) as u8;
272                    let lo = type_idx as u8;
273                    self.chunk.code.push(hi);
274                    self.chunk.code.push(lo);
275                }
276            }
277        }
278    }
279
280    /// Serialize a list of ShapeFields into a spec string for __assert_shape.
281    /// Format: `name:string,age:int,active:?bool,addr:{city:string,zip:string}`
282    fn shape_to_spec_string(fields: &[harn_parser::ShapeField]) -> String {
283        fields
284            .iter()
285            .map(|f| {
286                let opt = if f.optional { "?" } else { "" };
287                let type_str = Self::type_expr_to_spec(&f.type_expr);
288                format!("{}:{}{}", f.name, opt, type_str)
289            })
290            .collect::<Vec<_>>()
291            .join(",")
292    }
293
294    /// Convert a TypeExpr into a spec string fragment for shape validation.
295    fn type_expr_to_spec(type_expr: &harn_parser::TypeExpr) -> String {
296        match type_expr {
297            harn_parser::TypeExpr::Named(name) => name.clone(),
298            harn_parser::TypeExpr::Shape(fields) => {
299                let inner = Self::shape_to_spec_string(fields);
300                format!("{{{}}}", inner)
301            }
302            harn_parser::TypeExpr::List(_) => "list".to_string(),
303            harn_parser::TypeExpr::DictType(_, _) => "dict".to_string(),
304            harn_parser::TypeExpr::Union(members) => {
305                // Serialize union as "type1|type2|type3" for runtime validation
306                members
307                    .iter()
308                    .map(Self::type_expr_to_spec)
309                    .collect::<Vec<_>>()
310                    .join("|")
311            }
312            harn_parser::TypeExpr::FnType { .. } => "closure".to_string(),
313        }
314    }
315
316    /// Convert a TypeExpr to a runtime type name string for CheckType.
317    fn type_expr_to_runtime_name(type_expr: &harn_parser::TypeExpr) -> Option<String> {
318        match type_expr {
319            harn_parser::TypeExpr::Named(name) => match name.as_str() {
320                "int" | "float" | "string" | "bool" | "list" | "dict" | "set" | "nil"
321                | "closure" => Some(name.clone()),
322                _ => None, // Unknown types are not checked at runtime
323            },
324            _ => None, // Union types, shapes, etc. are not checked at runtime
325        }
326    }
327
328    /// Emit the extra u16 type name index after a TryCatchSetup jump.
329    fn emit_type_name_extra(&mut self, type_name_idx: u16) {
330        let hi = (type_name_idx >> 8) as u8;
331        let lo = type_name_idx as u8;
332        self.chunk.code.push(hi);
333        self.chunk.code.push(lo);
334        self.chunk.lines.push(self.line);
335        self.chunk.columns.push(self.column);
336        self.chunk.lines.push(self.line);
337        self.chunk.columns.push(self.column);
338    }
339
340    /// Compile a try/catch body block (produces a value on the stack).
341    fn compile_try_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
342        if body.is_empty() {
343            self.chunk.emit(Op::Nil, self.line);
344        } else {
345            self.compile_block(body)?;
346            if !Self::produces_value(&body.last().unwrap().node) {
347                self.chunk.emit(Op::Nil, self.line);
348            }
349        }
350        Ok(())
351    }
352
353    /// Compile catch error binding (error value is on stack from handler).
354    fn compile_catch_binding(&mut self, error_var: &Option<String>) -> Result<(), CompileError> {
355        if let Some(var_name) = error_var {
356            let idx = self.chunk.add_constant(Constant::String(var_name.clone()));
357            self.chunk.emit_u16(Op::DefLet, idx, self.line);
358        } else {
359            self.chunk.emit(Op::Pop, self.line);
360        }
361        Ok(())
362    }
363
364    /// Compile finally body inline, discarding its result value.
365    fn compile_finally_inline(&mut self, finally_body: &[SNode]) -> Result<(), CompileError> {
366        if !finally_body.is_empty() {
367            self.compile_block(finally_body)?;
368            // Finally body's value is discarded — only the try/catch value matters
369            if Self::produces_value(&finally_body.last().unwrap().node) {
370                self.chunk.emit(Op::Pop, self.line);
371            }
372        }
373        Ok(())
374    }
375
376    /// Compile rethrow pattern: save error to temp var, run finally, re-throw.
377    fn compile_rethrow_with_finally(&mut self, finally_body: &[SNode]) -> Result<(), CompileError> {
378        // Error is on the stack from the handler
379        self.temp_counter += 1;
380        let temp_name = format!("__finally_err_{}__", self.temp_counter);
381        let err_idx = self.chunk.add_constant(Constant::String(temp_name.clone()));
382        self.chunk.emit_u16(Op::DefVar, err_idx, self.line);
383        self.compile_finally_inline(finally_body)?;
384        let get_idx = self.chunk.add_constant(Constant::String(temp_name));
385        self.chunk.emit_u16(Op::GetVar, get_idx, self.line);
386        self.chunk.emit(Op::Throw, self.line);
387        Ok(())
388    }
389
390    fn compile_block(&mut self, stmts: &[SNode]) -> Result<(), CompileError> {
391        for (i, snode) in stmts.iter().enumerate() {
392            self.compile_node(snode)?;
393            let is_last = i == stmts.len() - 1;
394            if is_last {
395                // If the last statement doesn't produce a value, push nil
396                // so the block always leaves exactly one value on the stack.
397                if !Self::produces_value(&snode.node) {
398                    self.chunk.emit(Op::Nil, self.line);
399                }
400            } else {
401                // Only pop if the statement leaves a value on the stack
402                if Self::produces_value(&snode.node) {
403                    self.chunk.emit(Op::Pop, self.line);
404                }
405            }
406        }
407        Ok(())
408    }
409
410    fn compile_node(&mut self, snode: &SNode) -> Result<(), CompileError> {
411        self.line = snode.span.line as u32;
412        self.column = snode.span.column as u32;
413        self.chunk.set_column(self.column);
414        match &snode.node {
415            Node::IntLiteral(n) => {
416                let idx = self.chunk.add_constant(Constant::Int(*n));
417                self.chunk.emit_u16(Op::Constant, idx, self.line);
418            }
419            Node::FloatLiteral(n) => {
420                let idx = self.chunk.add_constant(Constant::Float(*n));
421                self.chunk.emit_u16(Op::Constant, idx, self.line);
422            }
423            Node::StringLiteral(s) => {
424                let idx = self.chunk.add_constant(Constant::String(s.clone()));
425                self.chunk.emit_u16(Op::Constant, idx, self.line);
426            }
427            Node::BoolLiteral(true) => self.chunk.emit(Op::True, self.line),
428            Node::BoolLiteral(false) => self.chunk.emit(Op::False, self.line),
429            Node::NilLiteral => self.chunk.emit(Op::Nil, self.line),
430            Node::DurationLiteral(ms) => {
431                let idx = self.chunk.add_constant(Constant::Duration(*ms));
432                self.chunk.emit_u16(Op::Constant, idx, self.line);
433            }
434
435            Node::Identifier(name) => {
436                let idx = self.chunk.add_constant(Constant::String(name.clone()));
437                self.chunk.emit_u16(Op::GetVar, idx, self.line);
438            }
439
440            Node::LetBinding { pattern, value, .. } => {
441                self.compile_node(value)?;
442                self.compile_destructuring(pattern, false)?;
443            }
444
445            Node::VarBinding { pattern, value, .. } => {
446                self.compile_node(value)?;
447                self.compile_destructuring(pattern, true)?;
448            }
449
450            Node::Assignment {
451                target, value, op, ..
452            } => {
453                if let Node::Identifier(name) = &target.node {
454                    let idx = self.chunk.add_constant(Constant::String(name.clone()));
455                    if let Some(op) = op {
456                        self.chunk.emit_u16(Op::GetVar, idx, self.line);
457                        self.compile_node(value)?;
458                        self.emit_compound_op(op)?;
459                        self.chunk.emit_u16(Op::SetVar, idx, self.line);
460                    } else {
461                        self.compile_node(value)?;
462                        self.chunk.emit_u16(Op::SetVar, idx, self.line);
463                    }
464                } else if let Node::PropertyAccess { object, property } = &target.node {
465                    // obj.field = value → SetProperty
466                    if let Some(var_name) = self.root_var_name(object) {
467                        let var_idx = self.chunk.add_constant(Constant::String(var_name.clone()));
468                        let prop_idx = self.chunk.add_constant(Constant::String(property.clone()));
469                        if let Some(op) = op {
470                            // compound: obj.field += value
471                            self.compile_node(target)?; // push current obj.field
472                            self.compile_node(value)?;
473                            self.emit_compound_op(op)?;
474                        } else {
475                            self.compile_node(value)?;
476                        }
477                        // Stack: [new_value]
478                        // SetProperty reads var_idx from env, sets prop, writes back
479                        self.chunk.emit_u16(Op::SetProperty, prop_idx, self.line);
480                        // Encode the variable name index as a second u16
481                        let hi = (var_idx >> 8) as u8;
482                        let lo = var_idx as u8;
483                        self.chunk.code.push(hi);
484                        self.chunk.code.push(lo);
485                        self.chunk.lines.push(self.line);
486                        self.chunk.columns.push(self.column);
487                        self.chunk.lines.push(self.line);
488                        self.chunk.columns.push(self.column);
489                    }
490                } else if let Node::SubscriptAccess { object, index } = &target.node {
491                    // obj[idx] = value → SetSubscript
492                    if let Some(var_name) = self.root_var_name(object) {
493                        let var_idx = self.chunk.add_constant(Constant::String(var_name.clone()));
494                        if let Some(op) = op {
495                            self.compile_node(target)?;
496                            self.compile_node(value)?;
497                            self.emit_compound_op(op)?;
498                        } else {
499                            self.compile_node(value)?;
500                        }
501                        self.compile_node(index)?;
502                        self.chunk.emit_u16(Op::SetSubscript, var_idx, self.line);
503                    }
504                }
505            }
506
507            Node::BinaryOp { op, left, right } => {
508                // Short-circuit operators
509                match op.as_str() {
510                    "&&" => {
511                        self.compile_node(left)?;
512                        let jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
513                        self.chunk.emit(Op::Pop, self.line);
514                        self.compile_node(right)?;
515                        self.chunk.patch_jump(jump);
516                        // Normalize to bool
517                        self.chunk.emit(Op::Not, self.line);
518                        self.chunk.emit(Op::Not, self.line);
519                        return Ok(());
520                    }
521                    "||" => {
522                        self.compile_node(left)?;
523                        let jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
524                        self.chunk.emit(Op::Pop, self.line);
525                        self.compile_node(right)?;
526                        self.chunk.patch_jump(jump);
527                        self.chunk.emit(Op::Not, self.line);
528                        self.chunk.emit(Op::Not, self.line);
529                        return Ok(());
530                    }
531                    "??" => {
532                        self.compile_node(left)?;
533                        self.chunk.emit(Op::Dup, self.line);
534                        // Check if nil: push nil, compare
535                        self.chunk.emit(Op::Nil, self.line);
536                        self.chunk.emit(Op::NotEqual, self.line);
537                        let jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
538                        self.chunk.emit(Op::Pop, self.line); // pop the not-equal result
539                        self.chunk.emit(Op::Pop, self.line); // pop the nil value
540                        self.compile_node(right)?;
541                        let end = self.chunk.emit_jump(Op::Jump, self.line);
542                        self.chunk.patch_jump(jump);
543                        self.chunk.emit(Op::Pop, self.line); // pop the not-equal result
544                        self.chunk.patch_jump(end);
545                        return Ok(());
546                    }
547                    "|>" => {
548                        self.compile_node(left)?;
549                        // If the RHS contains `_` placeholders, desugar into a closure:
550                        //   value |> func(_, arg)  =>  value |> { __pipe -> func(__pipe, arg) }
551                        if contains_pipe_placeholder(right) {
552                            let replaced = replace_pipe_placeholder(right);
553                            let closure_node = SNode::dummy(Node::Closure {
554                                params: vec![TypedParam {
555                                    name: "__pipe".into(),
556                                    type_expr: None,
557                                    default_value: None,
558                                }],
559                                body: vec![replaced],
560                            });
561                            self.compile_node(&closure_node)?;
562                        } else {
563                            self.compile_node(right)?;
564                        }
565                        self.chunk.emit(Op::Pipe, self.line);
566                        return Ok(());
567                    }
568                    _ => {}
569                }
570
571                self.compile_node(left)?;
572                self.compile_node(right)?;
573                match op.as_str() {
574                    "+" => self.chunk.emit(Op::Add, self.line),
575                    "-" => self.chunk.emit(Op::Sub, self.line),
576                    "*" => self.chunk.emit(Op::Mul, self.line),
577                    "/" => self.chunk.emit(Op::Div, self.line),
578                    "%" => self.chunk.emit(Op::Mod, self.line),
579                    "==" => self.chunk.emit(Op::Equal, self.line),
580                    "!=" => self.chunk.emit(Op::NotEqual, self.line),
581                    "<" => self.chunk.emit(Op::Less, self.line),
582                    ">" => self.chunk.emit(Op::Greater, self.line),
583                    "<=" => self.chunk.emit(Op::LessEqual, self.line),
584                    ">=" => self.chunk.emit(Op::GreaterEqual, self.line),
585                    _ => {
586                        return Err(CompileError {
587                            message: format!("Unknown operator: {op}"),
588                            line: self.line,
589                        })
590                    }
591                }
592            }
593
594            Node::UnaryOp { op, operand } => {
595                self.compile_node(operand)?;
596                match op.as_str() {
597                    "-" => self.chunk.emit(Op::Negate, self.line),
598                    "!" => self.chunk.emit(Op::Not, self.line),
599                    _ => {}
600                }
601            }
602
603            Node::Ternary {
604                condition,
605                true_expr,
606                false_expr,
607            } => {
608                self.compile_node(condition)?;
609                let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
610                self.chunk.emit(Op::Pop, self.line);
611                self.compile_node(true_expr)?;
612                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
613                self.chunk.patch_jump(else_jump);
614                self.chunk.emit(Op::Pop, self.line);
615                self.compile_node(false_expr)?;
616                self.chunk.patch_jump(end_jump);
617            }
618
619            Node::FunctionCall { name, args } => {
620                let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
621                // Push function name as string constant
622                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
623                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
624
625                if has_spread {
626                    // Build the args into a single list using the flush-and-concat
627                    // pattern (same as ListLiteral with spreads).
628                    self.chunk.emit_u16(Op::BuildList, 0, self.line);
629                    let mut pending = 0u16;
630                    for arg in args {
631                        if let Node::Spread(inner) = &arg.node {
632                            if pending > 0 {
633                                self.chunk.emit_u16(Op::BuildList, pending, self.line);
634                                self.chunk.emit(Op::Add, self.line);
635                                pending = 0;
636                            }
637                            self.compile_node(inner)?;
638                            self.chunk.emit(Op::Dup, self.line);
639                            let assert_idx = self
640                                .chunk
641                                .add_constant(Constant::String("__assert_list".into()));
642                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
643                            self.chunk.emit(Op::Swap, self.line);
644                            self.chunk.emit_u8(Op::Call, 1, self.line);
645                            self.chunk.emit(Op::Pop, self.line);
646                            self.chunk.emit(Op::Add, self.line);
647                        } else {
648                            self.compile_node(arg)?;
649                            pending += 1;
650                        }
651                    }
652                    if pending > 0 {
653                        self.chunk.emit_u16(Op::BuildList, pending, self.line);
654                        self.chunk.emit(Op::Add, self.line);
655                    }
656                    self.chunk.emit(Op::CallSpread, self.line);
657                } else {
658                    // Push arguments normally
659                    for arg in args {
660                        self.compile_node(arg)?;
661                    }
662                    self.chunk.emit_u8(Op::Call, args.len() as u8, self.line);
663                }
664            }
665
666            Node::MethodCall {
667                object,
668                method,
669                args,
670            } => {
671                // Check if this is an enum variant construction with args: EnumName.Variant(args)
672                if let Node::Identifier(name) = &object.node {
673                    if self.enum_names.contains(name) {
674                        // Compile args, then BuildEnum
675                        for arg in args {
676                            self.compile_node(arg)?;
677                        }
678                        let enum_idx = self.chunk.add_constant(Constant::String(name.clone()));
679                        let var_idx = self.chunk.add_constant(Constant::String(method.clone()));
680                        self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
681                        let hi = (var_idx >> 8) as u8;
682                        let lo = var_idx as u8;
683                        self.chunk.code.push(hi);
684                        self.chunk.code.push(lo);
685                        self.chunk.lines.push(self.line);
686                        self.chunk.columns.push(self.column);
687                        self.chunk.lines.push(self.line);
688                        self.chunk.columns.push(self.column);
689                        let fc = args.len() as u16;
690                        let fhi = (fc >> 8) as u8;
691                        let flo = fc as u8;
692                        self.chunk.code.push(fhi);
693                        self.chunk.code.push(flo);
694                        self.chunk.lines.push(self.line);
695                        self.chunk.columns.push(self.column);
696                        self.chunk.lines.push(self.line);
697                        self.chunk.columns.push(self.column);
698                        return Ok(());
699                    }
700                }
701                let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
702                self.compile_node(object)?;
703                let name_idx = self.chunk.add_constant(Constant::String(method.clone()));
704                if has_spread {
705                    // Build args into a single list (same pattern as FunctionCall spread)
706                    self.chunk.emit_u16(Op::BuildList, 0, self.line);
707                    let mut pending = 0u16;
708                    for arg in args {
709                        if let Node::Spread(inner) = &arg.node {
710                            if pending > 0 {
711                                self.chunk.emit_u16(Op::BuildList, pending, self.line);
712                                self.chunk.emit(Op::Add, self.line);
713                                pending = 0;
714                            }
715                            self.compile_node(inner)?;
716                            self.chunk.emit(Op::Dup, self.line);
717                            let assert_idx = self
718                                .chunk
719                                .add_constant(Constant::String("__assert_list".into()));
720                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
721                            self.chunk.emit(Op::Swap, self.line);
722                            self.chunk.emit_u8(Op::Call, 1, self.line);
723                            self.chunk.emit(Op::Pop, self.line);
724                            self.chunk.emit(Op::Add, self.line);
725                        } else {
726                            self.compile_node(arg)?;
727                            pending += 1;
728                        }
729                    }
730                    if pending > 0 {
731                        self.chunk.emit_u16(Op::BuildList, pending, self.line);
732                        self.chunk.emit(Op::Add, self.line);
733                    }
734                    self.chunk
735                        .emit_u16(Op::MethodCallSpread, name_idx, self.line);
736                } else {
737                    for arg in args {
738                        self.compile_node(arg)?;
739                    }
740                    self.chunk
741                        .emit_method_call(name_idx, args.len() as u8, self.line);
742                }
743            }
744
745            Node::OptionalMethodCall {
746                object,
747                method,
748                args,
749            } => {
750                self.compile_node(object)?;
751                for arg in args {
752                    self.compile_node(arg)?;
753                }
754                let name_idx = self.chunk.add_constant(Constant::String(method.clone()));
755                self.chunk
756                    .emit_method_call_opt(name_idx, args.len() as u8, self.line);
757            }
758
759            Node::PropertyAccess { object, property } => {
760                // Check if this is an enum variant construction: EnumName.Variant
761                if let Node::Identifier(name) = &object.node {
762                    if self.enum_names.contains(name) {
763                        // Emit BuildEnum with 0 fields
764                        let enum_idx = self.chunk.add_constant(Constant::String(name.clone()));
765                        let var_idx = self.chunk.add_constant(Constant::String(property.clone()));
766                        self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
767                        let hi = (var_idx >> 8) as u8;
768                        let lo = var_idx as u8;
769                        self.chunk.code.push(hi);
770                        self.chunk.code.push(lo);
771                        self.chunk.lines.push(self.line);
772                        self.chunk.columns.push(self.column);
773                        self.chunk.lines.push(self.line);
774                        self.chunk.columns.push(self.column);
775                        // 0 fields
776                        self.chunk.code.push(0);
777                        self.chunk.code.push(0);
778                        self.chunk.lines.push(self.line);
779                        self.chunk.columns.push(self.column);
780                        self.chunk.lines.push(self.line);
781                        self.chunk.columns.push(self.column);
782                        return Ok(());
783                    }
784                }
785                self.compile_node(object)?;
786                let idx = self.chunk.add_constant(Constant::String(property.clone()));
787                self.chunk.emit_u16(Op::GetProperty, idx, self.line);
788            }
789
790            Node::OptionalPropertyAccess { object, property } => {
791                self.compile_node(object)?;
792                let idx = self.chunk.add_constant(Constant::String(property.clone()));
793                self.chunk.emit_u16(Op::GetPropertyOpt, idx, self.line);
794            }
795
796            Node::SubscriptAccess { object, index } => {
797                self.compile_node(object)?;
798                self.compile_node(index)?;
799                self.chunk.emit(Op::Subscript, self.line);
800            }
801
802            Node::SliceAccess { object, start, end } => {
803                self.compile_node(object)?;
804                if let Some(s) = start {
805                    self.compile_node(s)?;
806                } else {
807                    self.chunk.emit(Op::Nil, self.line);
808                }
809                if let Some(e) = end {
810                    self.compile_node(e)?;
811                } else {
812                    self.chunk.emit(Op::Nil, self.line);
813                }
814                self.chunk.emit(Op::Slice, self.line);
815            }
816
817            Node::IfElse {
818                condition,
819                then_body,
820                else_body,
821            } => {
822                self.compile_node(condition)?;
823                let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
824                self.chunk.emit(Op::Pop, self.line);
825                self.compile_block(then_body)?;
826                if let Some(else_body) = else_body {
827                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
828                    self.chunk.patch_jump(else_jump);
829                    self.chunk.emit(Op::Pop, self.line);
830                    self.compile_block(else_body)?;
831                    self.chunk.patch_jump(end_jump);
832                } else {
833                    self.chunk.patch_jump(else_jump);
834                    self.chunk.emit(Op::Pop, self.line);
835                    self.chunk.emit(Op::Nil, self.line);
836                }
837            }
838
839            Node::WhileLoop { condition, body } => {
840                let loop_start = self.chunk.current_offset();
841                self.loop_stack.push(LoopContext {
842                    start_offset: loop_start,
843                    break_patches: Vec::new(),
844                    has_iterator: false,
845                    handler_depth: self.handler_depth,
846                    finally_depth: self.finally_bodies.len(),
847                });
848                self.compile_node(condition)?;
849                let exit_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
850                self.chunk.emit(Op::Pop, self.line); // pop condition
851                                                     // Compile body statements, popping all results
852                for sn in body {
853                    self.compile_node(sn)?;
854                    if Self::produces_value(&sn.node) {
855                        self.chunk.emit(Op::Pop, self.line);
856                    }
857                }
858                // Jump back to condition
859                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
860                self.chunk.patch_jump(exit_jump);
861                self.chunk.emit(Op::Pop, self.line); // pop condition
862                                                     // Patch all break jumps to here
863                let ctx = self.loop_stack.pop().unwrap();
864                for patch_pos in ctx.break_patches {
865                    self.chunk.patch_jump(patch_pos);
866                }
867                self.chunk.emit(Op::Nil, self.line);
868            }
869
870            Node::ForIn {
871                pattern,
872                iterable,
873                body,
874            } => {
875                // Compile iterable
876                self.compile_node(iterable)?;
877                // Initialize iterator
878                self.chunk.emit(Op::IterInit, self.line);
879                let loop_start = self.chunk.current_offset();
880                self.loop_stack.push(LoopContext {
881                    start_offset: loop_start,
882                    break_patches: Vec::new(),
883                    has_iterator: true,
884                    handler_depth: self.handler_depth,
885                    finally_depth: self.finally_bodies.len(),
886                });
887                // Try to get next item — jumps to end if exhausted
888                let exit_jump_pos = self.chunk.emit_jump(Op::IterNext, self.line);
889                // Define loop variable(s) with current item (item is on stack from IterNext)
890                self.compile_destructuring(pattern, true)?;
891                // Compile body statements, popping all results
892                for sn in body {
893                    self.compile_node(sn)?;
894                    if Self::produces_value(&sn.node) {
895                        self.chunk.emit(Op::Pop, self.line);
896                    }
897                }
898                // Loop back
899                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
900                self.chunk.patch_jump(exit_jump_pos);
901                // Patch all break jumps to here
902                let ctx = self.loop_stack.pop().unwrap();
903                for patch_pos in ctx.break_patches {
904                    self.chunk.patch_jump(patch_pos);
905                }
906                // Push nil as result (iterator state was consumed)
907                self.chunk.emit(Op::Nil, self.line);
908            }
909
910            Node::ReturnStmt { value } => {
911                let has_pending_finally = !self.finally_bodies.is_empty();
912
913                if has_pending_finally {
914                    // Inside try-finally: compile value, save to temp,
915                    // run pending finallys, restore value, then return.
916                    if let Some(val) = value {
917                        self.compile_node(val)?;
918                    } else {
919                        self.chunk.emit(Op::Nil, self.line);
920                    }
921                    self.temp_counter += 1;
922                    let temp_name = format!("__return_val_{}__", self.temp_counter);
923                    let save_idx = self.chunk.add_constant(Constant::String(temp_name.clone()));
924                    self.chunk.emit_u16(Op::DefVar, save_idx, self.line);
925                    // Emit all pending finallys (innermost first = reverse order)
926                    let finallys: Vec<_> = self.finally_bodies.iter().rev().cloned().collect();
927                    for fb in &finallys {
928                        self.compile_finally_inline(fb)?;
929                    }
930                    let restore_idx = self.chunk.add_constant(Constant::String(temp_name));
931                    self.chunk.emit_u16(Op::GetVar, restore_idx, self.line);
932                    self.chunk.emit(Op::Return, self.line);
933                } else {
934                    // No pending finally — original behavior with tail call optimization
935                    if let Some(val) = value {
936                        if let Node::FunctionCall { name, args } = &val.node {
937                            let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
938                            self.chunk.emit_u16(Op::Constant, name_idx, self.line);
939                            for arg in args {
940                                self.compile_node(arg)?;
941                            }
942                            self.chunk
943                                .emit_u8(Op::TailCall, args.len() as u8, self.line);
944                        } else if let Node::BinaryOp { op, left, right } = &val.node {
945                            if op == "|>" {
946                                self.compile_node(left)?;
947                                self.compile_node(right)?;
948                                self.chunk.emit(Op::Swap, self.line);
949                                self.chunk.emit_u8(Op::TailCall, 1, self.line);
950                            } else {
951                                self.compile_node(val)?;
952                            }
953                        } else {
954                            self.compile_node(val)?;
955                        }
956                    } else {
957                        self.chunk.emit(Op::Nil, self.line);
958                    }
959                    self.chunk.emit(Op::Return, self.line);
960                }
961            }
962
963            Node::BreakStmt => {
964                if self.loop_stack.is_empty() {
965                    return Err(CompileError {
966                        message: "break outside of loop".to_string(),
967                        line: self.line,
968                    });
969                }
970                // Copy values out to avoid borrow conflict
971                let ctx = self.loop_stack.last().unwrap();
972                let finally_depth = ctx.finally_depth;
973                let handler_depth = ctx.handler_depth;
974                let has_iterator = ctx.has_iterator;
975                // Pop exception handlers that were pushed inside the loop
976                for _ in handler_depth..self.handler_depth {
977                    self.chunk.emit(Op::PopHandler, self.line);
978                }
979                // Emit pending finallys that are inside the loop
980                if self.finally_bodies.len() > finally_depth {
981                    let finallys: Vec<_> = self.finally_bodies[finally_depth..]
982                        .iter()
983                        .rev()
984                        .cloned()
985                        .collect();
986                    for fb in &finallys {
987                        self.compile_finally_inline(fb)?;
988                    }
989                }
990                if has_iterator {
991                    self.chunk.emit(Op::PopIterator, self.line);
992                }
993                let patch = self.chunk.emit_jump(Op::Jump, self.line);
994                self.loop_stack
995                    .last_mut()
996                    .unwrap()
997                    .break_patches
998                    .push(patch);
999            }
1000
1001            Node::ContinueStmt => {
1002                if self.loop_stack.is_empty() {
1003                    return Err(CompileError {
1004                        message: "continue outside of loop".to_string(),
1005                        line: self.line,
1006                    });
1007                }
1008                let ctx = self.loop_stack.last().unwrap();
1009                let finally_depth = ctx.finally_depth;
1010                let handler_depth = ctx.handler_depth;
1011                let loop_start = ctx.start_offset;
1012                for _ in handler_depth..self.handler_depth {
1013                    self.chunk.emit(Op::PopHandler, self.line);
1014                }
1015                if self.finally_bodies.len() > finally_depth {
1016                    let finallys: Vec<_> = self.finally_bodies[finally_depth..]
1017                        .iter()
1018                        .rev()
1019                        .cloned()
1020                        .collect();
1021                    for fb in &finallys {
1022                        self.compile_finally_inline(fb)?;
1023                    }
1024                }
1025                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
1026            }
1027
1028            Node::ListLiteral(elements) => {
1029                let has_spread = elements.iter().any(|e| matches!(&e.node, Node::Spread(_)));
1030                if !has_spread {
1031                    for el in elements {
1032                        self.compile_node(el)?;
1033                    }
1034                    self.chunk
1035                        .emit_u16(Op::BuildList, elements.len() as u16, self.line);
1036                } else {
1037                    // Build with spreads: accumulate segments into lists and concat
1038                    // Start with empty list
1039                    self.chunk.emit_u16(Op::BuildList, 0, self.line);
1040                    let mut pending = 0u16;
1041                    for el in elements {
1042                        if let Node::Spread(inner) = &el.node {
1043                            // First, build list from pending non-spread elements
1044                            if pending > 0 {
1045                                self.chunk.emit_u16(Op::BuildList, pending, self.line);
1046                                // Concat accumulated + pending segment
1047                                self.chunk.emit(Op::Add, self.line);
1048                                pending = 0;
1049                            }
1050                            // Concat with the spread expression (with type check)
1051                            self.compile_node(inner)?;
1052                            self.chunk.emit(Op::Dup, self.line);
1053                            let assert_idx = self
1054                                .chunk
1055                                .add_constant(Constant::String("__assert_list".into()));
1056                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
1057                            self.chunk.emit(Op::Swap, self.line);
1058                            self.chunk.emit_u8(Op::Call, 1, self.line);
1059                            self.chunk.emit(Op::Pop, self.line);
1060                            self.chunk.emit(Op::Add, self.line);
1061                        } else {
1062                            self.compile_node(el)?;
1063                            pending += 1;
1064                        }
1065                    }
1066                    if pending > 0 {
1067                        self.chunk.emit_u16(Op::BuildList, pending, self.line);
1068                        self.chunk.emit(Op::Add, self.line);
1069                    }
1070                }
1071            }
1072
1073            Node::DictLiteral(entries) => {
1074                let has_spread = entries
1075                    .iter()
1076                    .any(|e| matches!(&e.value.node, Node::Spread(_)));
1077                if !has_spread {
1078                    for entry in entries {
1079                        self.compile_node(&entry.key)?;
1080                        self.compile_node(&entry.value)?;
1081                    }
1082                    self.chunk
1083                        .emit_u16(Op::BuildDict, entries.len() as u16, self.line);
1084                } else {
1085                    // Build with spreads: use empty dict + Add for merging
1086                    self.chunk.emit_u16(Op::BuildDict, 0, self.line);
1087                    let mut pending = 0u16;
1088                    for entry in entries {
1089                        if let Node::Spread(inner) = &entry.value.node {
1090                            // Flush pending entries
1091                            if pending > 0 {
1092                                self.chunk.emit_u16(Op::BuildDict, pending, self.line);
1093                                self.chunk.emit(Op::Add, self.line);
1094                                pending = 0;
1095                            }
1096                            // Merge spread dict via Add (with type check)
1097                            self.compile_node(inner)?;
1098                            self.chunk.emit(Op::Dup, self.line);
1099                            let assert_idx = self
1100                                .chunk
1101                                .add_constant(Constant::String("__assert_dict".into()));
1102                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
1103                            self.chunk.emit(Op::Swap, self.line);
1104                            self.chunk.emit_u8(Op::Call, 1, self.line);
1105                            self.chunk.emit(Op::Pop, self.line);
1106                            self.chunk.emit(Op::Add, self.line);
1107                        } else {
1108                            self.compile_node(&entry.key)?;
1109                            self.compile_node(&entry.value)?;
1110                            pending += 1;
1111                        }
1112                    }
1113                    if pending > 0 {
1114                        self.chunk.emit_u16(Op::BuildDict, pending, self.line);
1115                        self.chunk.emit(Op::Add, self.line);
1116                    }
1117                }
1118            }
1119
1120            Node::InterpolatedString(segments) => {
1121                let mut part_count = 0u16;
1122                for seg in segments {
1123                    match seg {
1124                        StringSegment::Literal(s) => {
1125                            let idx = self.chunk.add_constant(Constant::String(s.clone()));
1126                            self.chunk.emit_u16(Op::Constant, idx, self.line);
1127                            part_count += 1;
1128                        }
1129                        StringSegment::Expression(expr_str) => {
1130                            // Parse and compile the embedded expression
1131                            let mut lexer = harn_lexer::Lexer::new(expr_str);
1132                            if let Ok(tokens) = lexer.tokenize() {
1133                                let mut parser = harn_parser::Parser::new(tokens);
1134                                if let Ok(snode) = parser.parse_single_expression() {
1135                                    self.compile_node(&snode)?;
1136                                    // Convert result to string for concatenation
1137                                    let to_str = self
1138                                        .chunk
1139                                        .add_constant(Constant::String("to_string".into()));
1140                                    self.chunk.emit_u16(Op::Constant, to_str, self.line);
1141                                    self.chunk.emit(Op::Swap, self.line);
1142                                    self.chunk.emit_u8(Op::Call, 1, self.line);
1143                                    part_count += 1;
1144                                } else {
1145                                    // Fallback: treat as literal string
1146                                    let idx =
1147                                        self.chunk.add_constant(Constant::String(expr_str.clone()));
1148                                    self.chunk.emit_u16(Op::Constant, idx, self.line);
1149                                    part_count += 1;
1150                                }
1151                            }
1152                        }
1153                    }
1154                }
1155                if part_count > 1 {
1156                    self.chunk.emit_u16(Op::Concat, part_count, self.line);
1157                }
1158            }
1159
1160            Node::FnDecl {
1161                name, params, body, ..
1162            } => {
1163                // Compile function body into a separate chunk
1164                let mut fn_compiler = Compiler::new();
1165                fn_compiler.enum_names = self.enum_names.clone();
1166                fn_compiler.emit_default_preamble(params)?;
1167                fn_compiler.emit_type_checks(params);
1168                fn_compiler.compile_block(body)?;
1169                fn_compiler.chunk.emit(Op::Nil, self.line);
1170                fn_compiler.chunk.emit(Op::Return, self.line);
1171
1172                let func = CompiledFunction {
1173                    name: name.clone(),
1174                    params: TypedParam::names(params),
1175                    default_start: TypedParam::default_start(params),
1176                    chunk: fn_compiler.chunk,
1177                };
1178                let fn_idx = self.chunk.functions.len();
1179                self.chunk.functions.push(func);
1180
1181                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1182                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1183                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1184            }
1185
1186            Node::Closure { params, body } => {
1187                let mut fn_compiler = Compiler::new();
1188                fn_compiler.enum_names = self.enum_names.clone();
1189                fn_compiler.emit_default_preamble(params)?;
1190                fn_compiler.emit_type_checks(params);
1191                fn_compiler.compile_block(body)?;
1192                // If block didn't end with return, the last value is on the stack
1193                fn_compiler.chunk.emit(Op::Return, self.line);
1194
1195                let func = CompiledFunction {
1196                    name: "<closure>".to_string(),
1197                    params: TypedParam::names(params),
1198                    default_start: TypedParam::default_start(params),
1199                    chunk: fn_compiler.chunk,
1200                };
1201                let fn_idx = self.chunk.functions.len();
1202                self.chunk.functions.push(func);
1203
1204                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1205            }
1206
1207            Node::ThrowStmt { value } => {
1208                self.compile_node(value)?;
1209                self.chunk.emit(Op::Throw, self.line);
1210            }
1211
1212            Node::MatchExpr { value, arms } => {
1213                self.compile_node(value)?;
1214                let mut end_jumps = Vec::new();
1215                for arm in arms {
1216                    match &arm.pattern.node {
1217                        // Wildcard `_` — always matches
1218                        Node::Identifier(name) if name == "_" => {
1219                            self.chunk.emit(Op::Pop, self.line); // pop match value
1220                            self.compile_match_body(&arm.body)?;
1221                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1222                        }
1223                        // Enum destructuring: EnumConstruct pattern
1224                        Node::EnumConstruct {
1225                            enum_name,
1226                            variant,
1227                            args: pat_args,
1228                        } => {
1229                            // Check if the match value is this enum variant
1230                            self.chunk.emit(Op::Dup, self.line);
1231                            let en_idx =
1232                                self.chunk.add_constant(Constant::String(enum_name.clone()));
1233                            let vn_idx = self.chunk.add_constant(Constant::String(variant.clone()));
1234                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1235                            let hi = (vn_idx >> 8) as u8;
1236                            let lo = vn_idx as u8;
1237                            self.chunk.code.push(hi);
1238                            self.chunk.code.push(lo);
1239                            self.chunk.lines.push(self.line);
1240                            self.chunk.columns.push(self.column);
1241                            self.chunk.lines.push(self.line);
1242                            self.chunk.columns.push(self.column);
1243                            // Stack: [match_value, bool]
1244                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1245                            self.chunk.emit(Op::Pop, self.line); // pop bool
1246
1247                            // Destructure: bind field variables from the enum's fields
1248                            // The match value is still on the stack; we need to extract fields
1249                            for (i, pat_arg) in pat_args.iter().enumerate() {
1250                                if let Node::Identifier(binding_name) = &pat_arg.node {
1251                                    // Dup the match value, get .fields, subscript [i]
1252                                    self.chunk.emit(Op::Dup, self.line);
1253                                    let fields_idx = self
1254                                        .chunk
1255                                        .add_constant(Constant::String("fields".to_string()));
1256                                    self.chunk.emit_u16(Op::GetProperty, fields_idx, self.line);
1257                                    let idx_const =
1258                                        self.chunk.add_constant(Constant::Int(i as i64));
1259                                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1260                                    self.chunk.emit(Op::Subscript, self.line);
1261                                    let name_idx = self
1262                                        .chunk
1263                                        .add_constant(Constant::String(binding_name.clone()));
1264                                    self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1265                                }
1266                            }
1267
1268                            self.chunk.emit(Op::Pop, self.line); // pop match value
1269                            self.compile_match_body(&arm.body)?;
1270                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1271                            self.chunk.patch_jump(skip);
1272                            self.chunk.emit(Op::Pop, self.line); // pop bool
1273                        }
1274                        // Enum variant without args: PropertyAccess(EnumName, Variant)
1275                        Node::PropertyAccess { object, property } if matches!(&object.node, Node::Identifier(n) if self.enum_names.contains(n)) =>
1276                        {
1277                            let enum_name = if let Node::Identifier(n) = &object.node {
1278                                n.clone()
1279                            } else {
1280                                unreachable!()
1281                            };
1282                            self.chunk.emit(Op::Dup, self.line);
1283                            let en_idx = self.chunk.add_constant(Constant::String(enum_name));
1284                            let vn_idx =
1285                                self.chunk.add_constant(Constant::String(property.clone()));
1286                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1287                            let hi = (vn_idx >> 8) as u8;
1288                            let lo = vn_idx as u8;
1289                            self.chunk.code.push(hi);
1290                            self.chunk.code.push(lo);
1291                            self.chunk.lines.push(self.line);
1292                            self.chunk.columns.push(self.column);
1293                            self.chunk.lines.push(self.line);
1294                            self.chunk.columns.push(self.column);
1295                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1296                            self.chunk.emit(Op::Pop, self.line); // pop bool
1297                            self.chunk.emit(Op::Pop, self.line); // pop match value
1298                            self.compile_match_body(&arm.body)?;
1299                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1300                            self.chunk.patch_jump(skip);
1301                            self.chunk.emit(Op::Pop, self.line); // pop bool
1302                        }
1303                        // Enum destructuring via MethodCall: EnumName.Variant(bindings...)
1304                        // Parser produces MethodCall for EnumName.Variant(x) patterns
1305                        Node::MethodCall {
1306                            object,
1307                            method,
1308                            args: pat_args,
1309                        } if matches!(&object.node, Node::Identifier(n) if self.enum_names.contains(n)) =>
1310                        {
1311                            let enum_name = if let Node::Identifier(n) = &object.node {
1312                                n.clone()
1313                            } else {
1314                                unreachable!()
1315                            };
1316                            // Check if the match value is this enum variant
1317                            self.chunk.emit(Op::Dup, self.line);
1318                            let en_idx = self.chunk.add_constant(Constant::String(enum_name));
1319                            let vn_idx = self.chunk.add_constant(Constant::String(method.clone()));
1320                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1321                            let hi = (vn_idx >> 8) as u8;
1322                            let lo = vn_idx as u8;
1323                            self.chunk.code.push(hi);
1324                            self.chunk.code.push(lo);
1325                            self.chunk.lines.push(self.line);
1326                            self.chunk.columns.push(self.column);
1327                            self.chunk.lines.push(self.line);
1328                            self.chunk.columns.push(self.column);
1329                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1330                            self.chunk.emit(Op::Pop, self.line); // pop bool
1331
1332                            // Destructure: bind field variables
1333                            for (i, pat_arg) in pat_args.iter().enumerate() {
1334                                if let Node::Identifier(binding_name) = &pat_arg.node {
1335                                    self.chunk.emit(Op::Dup, self.line);
1336                                    let fields_idx = self
1337                                        .chunk
1338                                        .add_constant(Constant::String("fields".to_string()));
1339                                    self.chunk.emit_u16(Op::GetProperty, fields_idx, self.line);
1340                                    let idx_const =
1341                                        self.chunk.add_constant(Constant::Int(i as i64));
1342                                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1343                                    self.chunk.emit(Op::Subscript, self.line);
1344                                    let name_idx = self
1345                                        .chunk
1346                                        .add_constant(Constant::String(binding_name.clone()));
1347                                    self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1348                                }
1349                            }
1350
1351                            self.chunk.emit(Op::Pop, self.line); // pop match value
1352                            self.compile_match_body(&arm.body)?;
1353                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1354                            self.chunk.patch_jump(skip);
1355                            self.chunk.emit(Op::Pop, self.line); // pop bool
1356                        }
1357                        // Binding pattern: bare identifier (not a literal)
1358                        Node::Identifier(name) => {
1359                            // Bind the match value to this name, always matches
1360                            self.chunk.emit(Op::Dup, self.line); // dup for binding
1361                            let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1362                            self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1363                            self.chunk.emit(Op::Pop, self.line); // pop match value
1364                            self.compile_match_body(&arm.body)?;
1365                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1366                        }
1367                        // Dict pattern: {key: literal, key: binding, ...}
1368                        Node::DictLiteral(entries)
1369                            if entries
1370                                .iter()
1371                                .all(|e| matches!(&e.key.node, Node::StringLiteral(_))) =>
1372                        {
1373                            // Check type is dict: dup, call type_of, compare "dict"
1374                            self.chunk.emit(Op::Dup, self.line);
1375                            let typeof_idx =
1376                                self.chunk.add_constant(Constant::String("type_of".into()));
1377                            self.chunk.emit_u16(Op::Constant, typeof_idx, self.line);
1378                            self.chunk.emit(Op::Swap, self.line);
1379                            self.chunk.emit_u8(Op::Call, 1, self.line);
1380                            let dict_str = self.chunk.add_constant(Constant::String("dict".into()));
1381                            self.chunk.emit_u16(Op::Constant, dict_str, self.line);
1382                            self.chunk.emit(Op::Equal, self.line);
1383                            let skip_type = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1384                            self.chunk.emit(Op::Pop, self.line); // pop bool
1385
1386                            // Check literal constraints
1387                            let mut constraint_skips = Vec::new();
1388                            let mut bindings = Vec::new();
1389                            for entry in entries {
1390                                if let Node::StringLiteral(key) = &entry.key.node {
1391                                    match &entry.value.node {
1392                                        // Literal value → constraint: dict[key] == value
1393                                        Node::StringLiteral(_)
1394                                        | Node::IntLiteral(_)
1395                                        | Node::FloatLiteral(_)
1396                                        | Node::BoolLiteral(_)
1397                                        | Node::NilLiteral => {
1398                                            self.chunk.emit(Op::Dup, self.line);
1399                                            let key_idx = self
1400                                                .chunk
1401                                                .add_constant(Constant::String(key.clone()));
1402                                            self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1403                                            self.chunk.emit(Op::Subscript, self.line);
1404                                            self.compile_node(&entry.value)?;
1405                                            self.chunk.emit(Op::Equal, self.line);
1406                                            let skip =
1407                                                self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1408                                            self.chunk.emit(Op::Pop, self.line); // pop bool
1409                                            constraint_skips.push(skip);
1410                                        }
1411                                        // Identifier → binding: bind dict[key] to variable
1412                                        Node::Identifier(binding) => {
1413                                            bindings.push((key.clone(), binding.clone()));
1414                                        }
1415                                        _ => {
1416                                            // Complex expression constraint
1417                                            self.chunk.emit(Op::Dup, self.line);
1418                                            let key_idx = self
1419                                                .chunk
1420                                                .add_constant(Constant::String(key.clone()));
1421                                            self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1422                                            self.chunk.emit(Op::Subscript, self.line);
1423                                            self.compile_node(&entry.value)?;
1424                                            self.chunk.emit(Op::Equal, self.line);
1425                                            let skip =
1426                                                self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1427                                            self.chunk.emit(Op::Pop, self.line);
1428                                            constraint_skips.push(skip);
1429                                        }
1430                                    }
1431                                }
1432                            }
1433
1434                            // All constraints passed — emit bindings
1435                            for (key, binding) in &bindings {
1436                                self.chunk.emit(Op::Dup, self.line);
1437                                let key_idx =
1438                                    self.chunk.add_constant(Constant::String(key.clone()));
1439                                self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1440                                self.chunk.emit(Op::Subscript, self.line);
1441                                let name_idx =
1442                                    self.chunk.add_constant(Constant::String(binding.clone()));
1443                                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1444                            }
1445
1446                            self.chunk.emit(Op::Pop, self.line); // pop match value
1447                            self.compile_match_body(&arm.body)?;
1448                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1449
1450                            // All failures jump here: pop the false bool, leave match_value
1451                            let fail_target = self.chunk.code.len();
1452                            self.chunk.emit(Op::Pop, self.line); // pop bool
1453                                                                 // Patch all failure jumps to the shared cleanup point
1454                            for skip in constraint_skips {
1455                                self.chunk.patch_jump_to(skip, fail_target);
1456                            }
1457                            self.chunk.patch_jump_to(skip_type, fail_target);
1458                        }
1459                        // List pattern: [literal, binding, ...]
1460                        Node::ListLiteral(elements) => {
1461                            // Check type is list: dup, call type_of, compare "list"
1462                            self.chunk.emit(Op::Dup, self.line);
1463                            let typeof_idx =
1464                                self.chunk.add_constant(Constant::String("type_of".into()));
1465                            self.chunk.emit_u16(Op::Constant, typeof_idx, self.line);
1466                            self.chunk.emit(Op::Swap, self.line);
1467                            self.chunk.emit_u8(Op::Call, 1, self.line);
1468                            let list_str = self.chunk.add_constant(Constant::String("list".into()));
1469                            self.chunk.emit_u16(Op::Constant, list_str, self.line);
1470                            self.chunk.emit(Op::Equal, self.line);
1471                            let skip_type = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1472                            self.chunk.emit(Op::Pop, self.line); // pop bool
1473
1474                            // Check length: dup, call len, compare >= elements.len()
1475                            self.chunk.emit(Op::Dup, self.line);
1476                            let len_idx = self.chunk.add_constant(Constant::String("len".into()));
1477                            self.chunk.emit_u16(Op::Constant, len_idx, self.line);
1478                            self.chunk.emit(Op::Swap, self.line);
1479                            self.chunk.emit_u8(Op::Call, 1, self.line);
1480                            let count = self
1481                                .chunk
1482                                .add_constant(Constant::Int(elements.len() as i64));
1483                            self.chunk.emit_u16(Op::Constant, count, self.line);
1484                            self.chunk.emit(Op::GreaterEqual, self.line);
1485                            let skip_len = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1486                            self.chunk.emit(Op::Pop, self.line); // pop bool
1487
1488                            // Check literal constraints and collect bindings
1489                            let mut constraint_skips = Vec::new();
1490                            let mut bindings = Vec::new();
1491                            for (i, elem) in elements.iter().enumerate() {
1492                                match &elem.node {
1493                                    Node::Identifier(name) if name != "_" => {
1494                                        bindings.push((i, name.clone()));
1495                                    }
1496                                    Node::Identifier(_) => {} // wildcard _
1497                                    // Literal constraint
1498                                    _ => {
1499                                        self.chunk.emit(Op::Dup, self.line);
1500                                        let idx_const =
1501                                            self.chunk.add_constant(Constant::Int(i as i64));
1502                                        self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1503                                        self.chunk.emit(Op::Subscript, self.line);
1504                                        self.compile_node(elem)?;
1505                                        self.chunk.emit(Op::Equal, self.line);
1506                                        let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1507                                        self.chunk.emit(Op::Pop, self.line);
1508                                        constraint_skips.push(skip);
1509                                    }
1510                                }
1511                            }
1512
1513                            // Emit bindings
1514                            for (i, name) in &bindings {
1515                                self.chunk.emit(Op::Dup, self.line);
1516                                let idx_const = self.chunk.add_constant(Constant::Int(*i as i64));
1517                                self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1518                                self.chunk.emit(Op::Subscript, self.line);
1519                                let name_idx =
1520                                    self.chunk.add_constant(Constant::String(name.clone()));
1521                                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1522                            }
1523
1524                            self.chunk.emit(Op::Pop, self.line); // pop match value
1525                            self.compile_match_body(&arm.body)?;
1526                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1527
1528                            // All failures jump here: pop the false bool
1529                            let fail_target = self.chunk.code.len();
1530                            self.chunk.emit(Op::Pop, self.line); // pop bool
1531                            for skip in constraint_skips {
1532                                self.chunk.patch_jump_to(skip, fail_target);
1533                            }
1534                            self.chunk.patch_jump_to(skip_len, fail_target);
1535                            self.chunk.patch_jump_to(skip_type, fail_target);
1536                        }
1537                        // Literal/expression pattern — compare with Equal
1538                        _ => {
1539                            self.chunk.emit(Op::Dup, self.line);
1540                            self.compile_node(&arm.pattern)?;
1541                            self.chunk.emit(Op::Equal, self.line);
1542                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1543                            self.chunk.emit(Op::Pop, self.line); // pop bool
1544                            self.chunk.emit(Op::Pop, self.line); // pop match value
1545                            self.compile_match_body(&arm.body)?;
1546                            end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1547                            self.chunk.patch_jump(skip);
1548                            self.chunk.emit(Op::Pop, self.line); // pop bool
1549                        }
1550                    }
1551                }
1552                // No match — pop value, push nil
1553                self.chunk.emit(Op::Pop, self.line);
1554                self.chunk.emit(Op::Nil, self.line);
1555                for j in end_jumps {
1556                    self.chunk.patch_jump(j);
1557                }
1558            }
1559
1560            Node::RangeExpr {
1561                start,
1562                end,
1563                inclusive,
1564            } => {
1565                // Compile as __range__(start, end, inclusive_bool) builtin call
1566                let name_idx = self
1567                    .chunk
1568                    .add_constant(Constant::String("__range__".to_string()));
1569                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
1570                self.compile_node(start)?;
1571                self.compile_node(end)?;
1572                if *inclusive {
1573                    self.chunk.emit(Op::True, self.line);
1574                } else {
1575                    self.chunk.emit(Op::False, self.line);
1576                }
1577                self.chunk.emit_u8(Op::Call, 3, self.line);
1578            }
1579
1580            Node::GuardStmt {
1581                condition,
1582                else_body,
1583            } => {
1584                // guard condition else { body }
1585                // Compile condition; if truthy, skip else_body
1586                self.compile_node(condition)?;
1587                let skip_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
1588                self.chunk.emit(Op::Pop, self.line); // pop condition
1589                                                     // Compile else_body
1590                self.compile_block(else_body)?;
1591                // Pop result of else_body (guard is a statement, not expression)
1592                if !else_body.is_empty() && Self::produces_value(&else_body.last().unwrap().node) {
1593                    self.chunk.emit(Op::Pop, self.line);
1594                }
1595                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1596                self.chunk.patch_jump(skip_jump);
1597                self.chunk.emit(Op::Pop, self.line); // pop condition
1598                self.chunk.patch_jump(end_jump);
1599                self.chunk.emit(Op::Nil, self.line);
1600            }
1601
1602            Node::Block(stmts) => {
1603                if stmts.is_empty() {
1604                    self.chunk.emit(Op::Nil, self.line);
1605                } else {
1606                    self.compile_block(stmts)?;
1607                }
1608            }
1609
1610            Node::DeadlineBlock { duration, body } => {
1611                self.compile_node(duration)?;
1612                self.chunk.emit(Op::DeadlineSetup, self.line);
1613                if body.is_empty() {
1614                    self.chunk.emit(Op::Nil, self.line);
1615                } else {
1616                    self.compile_block(body)?;
1617                }
1618                self.chunk.emit(Op::DeadlineEnd, self.line);
1619            }
1620
1621            Node::MutexBlock { body } => {
1622                // v1: single-threaded, just compile the body
1623                if body.is_empty() {
1624                    self.chunk.emit(Op::Nil, self.line);
1625                } else {
1626                    // Compile body, but pop intermediate values and push nil at the end.
1627                    // The body typically contains statements (assignments) that don't produce values.
1628                    for sn in body {
1629                        self.compile_node(sn)?;
1630                        if Self::produces_value(&sn.node) {
1631                            self.chunk.emit(Op::Pop, self.line);
1632                        }
1633                    }
1634                    self.chunk.emit(Op::Nil, self.line);
1635                }
1636            }
1637
1638            Node::YieldExpr { .. } => {
1639                // v1: yield is host-integration only, emit nil
1640                self.chunk.emit(Op::Nil, self.line);
1641            }
1642
1643            Node::AskExpr { fields } => {
1644                // Compile as a dict literal and call llm_call builtin
1645                // For v1, just build the dict (llm_call requires async)
1646                for entry in fields {
1647                    self.compile_node(&entry.key)?;
1648                    self.compile_node(&entry.value)?;
1649                }
1650                self.chunk
1651                    .emit_u16(Op::BuildDict, fields.len() as u16, self.line);
1652            }
1653
1654            Node::EnumConstruct {
1655                enum_name,
1656                variant,
1657                args,
1658            } => {
1659                // Push field values onto the stack, then BuildEnum
1660                for arg in args {
1661                    self.compile_node(arg)?;
1662                }
1663                let enum_idx = self.chunk.add_constant(Constant::String(enum_name.clone()));
1664                let var_idx = self.chunk.add_constant(Constant::String(variant.clone()));
1665                // BuildEnum: enum_name_idx, variant_idx, field_count
1666                self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
1667                let hi = (var_idx >> 8) as u8;
1668                let lo = var_idx as u8;
1669                self.chunk.code.push(hi);
1670                self.chunk.code.push(lo);
1671                self.chunk.lines.push(self.line);
1672                self.chunk.columns.push(self.column);
1673                self.chunk.lines.push(self.line);
1674                self.chunk.columns.push(self.column);
1675                let fc = args.len() as u16;
1676                let fhi = (fc >> 8) as u8;
1677                let flo = fc as u8;
1678                self.chunk.code.push(fhi);
1679                self.chunk.code.push(flo);
1680                self.chunk.lines.push(self.line);
1681                self.chunk.columns.push(self.column);
1682                self.chunk.lines.push(self.line);
1683                self.chunk.columns.push(self.column);
1684            }
1685
1686            Node::StructConstruct {
1687                struct_name,
1688                fields,
1689            } => {
1690                // Build as a dict with a __struct__ key for metadata
1691                let struct_key = self
1692                    .chunk
1693                    .add_constant(Constant::String("__struct__".to_string()));
1694                let struct_val = self
1695                    .chunk
1696                    .add_constant(Constant::String(struct_name.clone()));
1697                self.chunk.emit_u16(Op::Constant, struct_key, self.line);
1698                self.chunk.emit_u16(Op::Constant, struct_val, self.line);
1699
1700                for entry in fields {
1701                    self.compile_node(&entry.key)?;
1702                    self.compile_node(&entry.value)?;
1703                }
1704                self.chunk
1705                    .emit_u16(Op::BuildDict, (fields.len() + 1) as u16, self.line);
1706            }
1707
1708            Node::ImportDecl { path } => {
1709                let idx = self.chunk.add_constant(Constant::String(path.clone()));
1710                self.chunk.emit_u16(Op::Import, idx, self.line);
1711            }
1712
1713            Node::SelectiveImport { names, path } => {
1714                let path_idx = self.chunk.add_constant(Constant::String(path.clone()));
1715                let names_str = names.join(",");
1716                let names_idx = self.chunk.add_constant(Constant::String(names_str));
1717                self.chunk
1718                    .emit_u16(Op::SelectiveImport, path_idx, self.line);
1719                let hi = (names_idx >> 8) as u8;
1720                let lo = names_idx as u8;
1721                self.chunk.code.push(hi);
1722                self.chunk.code.push(lo);
1723                self.chunk.lines.push(self.line);
1724                self.chunk.columns.push(self.column);
1725                self.chunk.lines.push(self.line);
1726                self.chunk.columns.push(self.column);
1727            }
1728
1729            Node::TryOperator { operand } => {
1730                self.compile_node(operand)?;
1731                self.chunk.emit(Op::TryUnwrap, self.line);
1732            }
1733
1734            Node::ImplBlock { type_name, methods } => {
1735                // Compile each method as a closure and store in __impl_TypeName dict.
1736                // Build key-value pairs on stack, then BuildDict.
1737                for method_sn in methods {
1738                    if let Node::FnDecl {
1739                        name, params, body, ..
1740                    } = &method_sn.node
1741                    {
1742                        // Method name key
1743                        let key_idx = self.chunk.add_constant(Constant::String(name.clone()));
1744                        self.chunk.emit_u16(Op::Constant, key_idx, self.line);
1745
1746                        // Compile method body as closure
1747                        let mut fn_compiler = Compiler::new();
1748                        fn_compiler.enum_names = self.enum_names.clone();
1749                        fn_compiler.emit_default_preamble(params)?;
1750                        fn_compiler.emit_type_checks(params);
1751                        fn_compiler.compile_block(body)?;
1752                        fn_compiler.chunk.emit(Op::Nil, self.line);
1753                        fn_compiler.chunk.emit(Op::Return, self.line);
1754
1755                        let func = CompiledFunction {
1756                            name: format!("{}.{}", type_name, name),
1757                            params: TypedParam::names(params),
1758                            default_start: TypedParam::default_start(params),
1759                            chunk: fn_compiler.chunk,
1760                        };
1761                        let fn_idx = self.chunk.functions.len();
1762                        self.chunk.functions.push(func);
1763                        self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1764                    }
1765                }
1766                let method_count = methods
1767                    .iter()
1768                    .filter(|m| matches!(m.node, Node::FnDecl { .. }))
1769                    .count();
1770                self.chunk
1771                    .emit_u16(Op::BuildDict, method_count as u16, self.line);
1772                let impl_name = format!("__impl_{}", type_name);
1773                let name_idx = self.chunk.add_constant(Constant::String(impl_name));
1774                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1775            }
1776
1777            Node::StructDecl { name, .. } => {
1778                // Compile a constructor function: StructName({field: val, ...}) -> StructInstance
1779                let mut fn_compiler = Compiler::new();
1780                fn_compiler.enum_names = self.enum_names.clone();
1781                let params = vec![TypedParam::untyped("__fields")];
1782                fn_compiler.emit_default_preamble(&params)?;
1783
1784                // Call __make_struct(struct_name, fields_dict) to tag the dict
1785                let make_idx = fn_compiler
1786                    .chunk
1787                    .add_constant(Constant::String("__make_struct".into()));
1788                fn_compiler
1789                    .chunk
1790                    .emit_u16(Op::Constant, make_idx, self.line);
1791                let sname_idx = fn_compiler
1792                    .chunk
1793                    .add_constant(Constant::String(name.clone()));
1794                fn_compiler
1795                    .chunk
1796                    .emit_u16(Op::Constant, sname_idx, self.line);
1797                let fields_idx = fn_compiler
1798                    .chunk
1799                    .add_constant(Constant::String("__fields".into()));
1800                fn_compiler
1801                    .chunk
1802                    .emit_u16(Op::GetVar, fields_idx, self.line);
1803                fn_compiler.chunk.emit_u8(Op::Call, 2, self.line);
1804                fn_compiler.chunk.emit(Op::Return, self.line);
1805
1806                let func = CompiledFunction {
1807                    name: name.clone(),
1808                    params: TypedParam::names(&params),
1809                    default_start: None,
1810                    chunk: fn_compiler.chunk,
1811                };
1812                let fn_idx = self.chunk.functions.len();
1813                self.chunk.functions.push(func);
1814                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1815                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1816                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1817            }
1818
1819            // Declarations that only register metadata (no runtime effect needed for v1)
1820            Node::Pipeline { .. }
1821            | Node::OverrideDecl { .. }
1822            | Node::TypeDecl { .. }
1823            | Node::EnumDecl { .. }
1824            | Node::InterfaceDecl { .. } => {
1825                self.chunk.emit(Op::Nil, self.line);
1826            }
1827
1828            Node::TryCatch {
1829                body,
1830                error_var,
1831                error_type,
1832                catch_body,
1833                finally_body,
1834            } => {
1835                // Extract the type name for typed catch (e.g., "AppError")
1836                let type_name = error_type.as_ref().and_then(|te| {
1837                    if let harn_parser::TypeExpr::Named(name) = te {
1838                        Some(name.clone())
1839                    } else {
1840                        None
1841                    }
1842                });
1843
1844                let type_name_idx = if let Some(ref tn) = type_name {
1845                    self.chunk.add_constant(Constant::String(tn.clone()))
1846                } else {
1847                    self.chunk.add_constant(Constant::String(String::new()))
1848                };
1849
1850                let has_catch = !catch_body.is_empty() || error_var.is_some();
1851                let has_finally = finally_body.is_some();
1852
1853                if has_catch && has_finally {
1854                    // === try-catch-finally ===
1855                    let finally_body = finally_body.as_ref().unwrap();
1856
1857                    // Push finally body onto pending stack for return/break handling
1858                    self.finally_bodies.push(finally_body.clone());
1859
1860                    // 1. TryCatchSetup for try body
1861                    self.handler_depth += 1;
1862                    let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1863                    self.emit_type_name_extra(type_name_idx);
1864
1865                    // 2. Compile try body
1866                    self.compile_try_body(body)?;
1867
1868                    // 3. PopHandler + inline finally (success path)
1869                    self.handler_depth -= 1;
1870                    self.chunk.emit(Op::PopHandler, self.line);
1871                    self.compile_finally_inline(finally_body)?;
1872                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1873
1874                    // 4. Catch entry
1875                    self.chunk.patch_jump(catch_jump);
1876                    self.compile_catch_binding(error_var)?;
1877
1878                    // 5. Inner try around catch body (so finally runs if catch throws)
1879                    self.handler_depth += 1;
1880                    let rethrow_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1881                    let empty_type = self.chunk.add_constant(Constant::String(String::new()));
1882                    self.emit_type_name_extra(empty_type);
1883
1884                    // 6. Compile catch body
1885                    self.compile_try_body(catch_body)?;
1886
1887                    // 7. PopHandler + inline finally (catch success path)
1888                    self.handler_depth -= 1;
1889                    self.chunk.emit(Op::PopHandler, self.line);
1890                    self.compile_finally_inline(finally_body)?;
1891                    let end_jump2 = self.chunk.emit_jump(Op::Jump, self.line);
1892
1893                    // 8. Rethrow handler: save error, run finally, re-throw
1894                    self.chunk.patch_jump(rethrow_jump);
1895                    self.compile_rethrow_with_finally(finally_body)?;
1896
1897                    self.chunk.patch_jump(end_jump);
1898                    self.chunk.patch_jump(end_jump2);
1899
1900                    self.finally_bodies.pop();
1901                } else if has_finally {
1902                    // === try-finally (no catch) ===
1903                    let finally_body = finally_body.as_ref().unwrap();
1904
1905                    self.finally_bodies.push(finally_body.clone());
1906
1907                    // 1. TryCatchSetup to error path
1908                    self.handler_depth += 1;
1909                    let error_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1910                    let empty_type = self.chunk.add_constant(Constant::String(String::new()));
1911                    self.emit_type_name_extra(empty_type);
1912
1913                    // 2. Compile try body
1914                    self.compile_try_body(body)?;
1915
1916                    // 3. PopHandler + inline finally (success path)
1917                    self.handler_depth -= 1;
1918                    self.chunk.emit(Op::PopHandler, self.line);
1919                    self.compile_finally_inline(finally_body)?;
1920                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1921
1922                    // 4. Error path: save error, run finally, re-throw
1923                    self.chunk.patch_jump(error_jump);
1924                    self.compile_rethrow_with_finally(finally_body)?;
1925
1926                    self.chunk.patch_jump(end_jump);
1927
1928                    self.finally_bodies.pop();
1929                } else {
1930                    // === try-catch (no finally) — original behavior ===
1931
1932                    // 1. TryCatchSetup
1933                    self.handler_depth += 1;
1934                    let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1935                    self.emit_type_name_extra(type_name_idx);
1936
1937                    // 2. Compile try body
1938                    self.compile_try_body(body)?;
1939
1940                    // 3. PopHandler + jump past catch
1941                    self.handler_depth -= 1;
1942                    self.chunk.emit(Op::PopHandler, self.line);
1943                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1944
1945                    // 4. Catch entry
1946                    self.chunk.patch_jump(catch_jump);
1947                    self.compile_catch_binding(error_var)?;
1948
1949                    // 5. Compile catch body
1950                    self.compile_try_body(catch_body)?;
1951
1952                    // 6. Patch end
1953                    self.chunk.patch_jump(end_jump);
1954                }
1955            }
1956
1957            Node::TryExpr { body } => {
1958                // try { body } — returns Result.Ok(value) or Result.Err(error)
1959
1960                // 1. Set up try-catch handler (untyped)
1961                self.handler_depth += 1;
1962                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
1963                let empty_type = self.chunk.add_constant(Constant::String(String::new()));
1964                self.emit_type_name_extra(empty_type);
1965
1966                // 2. Compile try body (leaves value on stack)
1967                self.compile_try_body(body)?;
1968
1969                // 3. PopHandler (success path)
1970                self.handler_depth -= 1;
1971                self.chunk.emit(Op::PopHandler, self.line);
1972
1973                // 4. Wrap in Result.Ok: push "Ok", swap, call Ok(value)
1974                let ok_idx = self.chunk.add_constant(Constant::String("Ok".to_string()));
1975                self.chunk.emit_u16(Op::Constant, ok_idx, self.line);
1976                self.chunk.emit(Op::Swap, self.line);
1977                self.chunk.emit_u8(Op::Call, 1, self.line);
1978
1979                // 5. Jump past error handler
1980                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1981
1982                // 6. Error handler: error value is on stack
1983                self.chunk.patch_jump(catch_jump);
1984
1985                // 7. Wrap in Result.Err: push "Err", swap, call Err(error)
1986                let err_idx = self.chunk.add_constant(Constant::String("Err".to_string()));
1987                self.chunk.emit_u16(Op::Constant, err_idx, self.line);
1988                self.chunk.emit(Op::Swap, self.line);
1989                self.chunk.emit_u8(Op::Call, 1, self.line);
1990
1991                // 8. Patch end
1992                self.chunk.patch_jump(end_jump);
1993            }
1994
1995            Node::Retry { count, body } => {
1996                // Compile count expression into a mutable counter variable
1997                self.compile_node(count)?;
1998                let counter_name = "__retry_counter__";
1999                let counter_idx = self
2000                    .chunk
2001                    .add_constant(Constant::String(counter_name.to_string()));
2002                self.chunk.emit_u16(Op::DefVar, counter_idx, self.line);
2003
2004                // Also store the last error for re-throwing
2005                self.chunk.emit(Op::Nil, self.line);
2006                let err_name = "__retry_last_error__";
2007                let err_idx = self
2008                    .chunk
2009                    .add_constant(Constant::String(err_name.to_string()));
2010                self.chunk.emit_u16(Op::DefVar, err_idx, self.line);
2011
2012                // Loop start
2013                let loop_start = self.chunk.current_offset();
2014
2015                // Set up try/catch (untyped - empty type name)
2016                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
2017                // Emit empty type name for untyped catch
2018                let empty_type = self.chunk.add_constant(Constant::String(String::new()));
2019                let hi = (empty_type >> 8) as u8;
2020                let lo = empty_type as u8;
2021                self.chunk.code.push(hi);
2022                self.chunk.code.push(lo);
2023                self.chunk.lines.push(self.line);
2024                self.chunk.columns.push(self.column);
2025                self.chunk.lines.push(self.line);
2026                self.chunk.columns.push(self.column);
2027
2028                // Compile body
2029                self.compile_block(body)?;
2030
2031                // Success: pop handler, jump to end
2032                self.chunk.emit(Op::PopHandler, self.line);
2033                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
2034
2035                // Catch handler
2036                self.chunk.patch_jump(catch_jump);
2037                // Save the error value for potential re-throw
2038                self.chunk.emit(Op::Dup, self.line);
2039                self.chunk.emit_u16(Op::SetVar, err_idx, self.line);
2040                // Pop the error value
2041                self.chunk.emit(Op::Pop, self.line);
2042
2043                // Decrement counter
2044                self.chunk.emit_u16(Op::GetVar, counter_idx, self.line);
2045                let one_idx = self.chunk.add_constant(Constant::Int(1));
2046                self.chunk.emit_u16(Op::Constant, one_idx, self.line);
2047                self.chunk.emit(Op::Sub, self.line);
2048                self.chunk.emit(Op::Dup, self.line);
2049                self.chunk.emit_u16(Op::SetVar, counter_idx, self.line);
2050
2051                // If counter > 0, jump to loop start
2052                let zero_idx = self.chunk.add_constant(Constant::Int(0));
2053                self.chunk.emit_u16(Op::Constant, zero_idx, self.line);
2054                self.chunk.emit(Op::Greater, self.line);
2055                let retry_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2056                self.chunk.emit(Op::Pop, self.line); // pop condition
2057                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
2058
2059                // No more retries — re-throw the last error
2060                self.chunk.patch_jump(retry_jump);
2061                self.chunk.emit(Op::Pop, self.line); // pop condition
2062                self.chunk.emit_u16(Op::GetVar, err_idx, self.line);
2063                self.chunk.emit(Op::Throw, self.line);
2064
2065                self.chunk.patch_jump(end_jump);
2066                // Push nil as the result of a successful retry block
2067                self.chunk.emit(Op::Nil, self.line);
2068            }
2069
2070            Node::Parallel {
2071                count,
2072                variable,
2073                body,
2074            } => {
2075                self.compile_node(count)?;
2076                let mut fn_compiler = Compiler::new();
2077                fn_compiler.enum_names = self.enum_names.clone();
2078                fn_compiler.compile_block(body)?;
2079                fn_compiler.chunk.emit(Op::Return, self.line);
2080                let params = vec![variable.clone().unwrap_or_else(|| "__i__".to_string())];
2081                let func = CompiledFunction {
2082                    name: "<parallel>".to_string(),
2083                    params,
2084                    default_start: None,
2085                    chunk: fn_compiler.chunk,
2086                };
2087                let fn_idx = self.chunk.functions.len();
2088                self.chunk.functions.push(func);
2089                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2090                self.chunk.emit(Op::Parallel, self.line);
2091            }
2092
2093            Node::ParallelMap {
2094                list,
2095                variable,
2096                body,
2097            } => {
2098                self.compile_node(list)?;
2099                let mut fn_compiler = Compiler::new();
2100                fn_compiler.enum_names = self.enum_names.clone();
2101                fn_compiler.compile_block(body)?;
2102                fn_compiler.chunk.emit(Op::Return, self.line);
2103                let func = CompiledFunction {
2104                    name: "<parallel_map>".to_string(),
2105                    params: vec![variable.clone()],
2106                    default_start: None,
2107                    chunk: fn_compiler.chunk,
2108                };
2109                let fn_idx = self.chunk.functions.len();
2110                self.chunk.functions.push(func);
2111                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2112                self.chunk.emit(Op::ParallelMap, self.line);
2113            }
2114
2115            Node::SpawnExpr { body } => {
2116                let mut fn_compiler = Compiler::new();
2117                fn_compiler.enum_names = self.enum_names.clone();
2118                fn_compiler.compile_block(body)?;
2119                fn_compiler.chunk.emit(Op::Return, self.line);
2120                let func = CompiledFunction {
2121                    name: "<spawn>".to_string(),
2122                    params: vec![],
2123                    default_start: None,
2124                    chunk: fn_compiler.chunk,
2125                };
2126                let fn_idx = self.chunk.functions.len();
2127                self.chunk.functions.push(func);
2128                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2129                self.chunk.emit(Op::Spawn, self.line);
2130            }
2131            Node::SelectExpr {
2132                cases,
2133                timeout,
2134                default_body,
2135            } => {
2136                // Desugar select into: builtin call + index-based dispatch.
2137                //
2138                // Step 1: Push builtin name, compile channel list, optionally
2139                //         compile timeout duration, then Call.
2140                // Step 2: Store result dict in temp, dispatch on result.index.
2141
2142                let builtin_name = if timeout.is_some() {
2143                    "__select_timeout"
2144                } else if default_body.is_some() {
2145                    "__select_try"
2146                } else {
2147                    "__select_list"
2148                };
2149
2150                // Push builtin name (callee goes below args on stack)
2151                let name_idx = self
2152                    .chunk
2153                    .add_constant(Constant::String(builtin_name.into()));
2154                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
2155
2156                // Build channel list (arg 1)
2157                for case in cases {
2158                    self.compile_node(&case.channel)?;
2159                }
2160                self.chunk
2161                    .emit_u16(Op::BuildList, cases.len() as u16, self.line);
2162
2163                // If timeout, compile duration (arg 2)
2164                if let Some((duration_expr, _)) = timeout {
2165                    self.compile_node(duration_expr)?;
2166                    self.chunk.emit_u8(Op::Call, 2, self.line);
2167                } else {
2168                    self.chunk.emit_u8(Op::Call, 1, self.line);
2169                }
2170
2171                // Store result in temp var
2172                self.temp_counter += 1;
2173                let result_name = format!("__sel_result_{}__", self.temp_counter);
2174                let result_idx = self
2175                    .chunk
2176                    .add_constant(Constant::String(result_name.clone()));
2177                self.chunk.emit_u16(Op::DefVar, result_idx, self.line);
2178
2179                // Dispatch on result.index
2180                let mut end_jumps = Vec::new();
2181
2182                for (i, case) in cases.iter().enumerate() {
2183                    let get_r = self
2184                        .chunk
2185                        .add_constant(Constant::String(result_name.clone()));
2186                    self.chunk.emit_u16(Op::GetVar, get_r, self.line);
2187                    let idx_prop = self.chunk.add_constant(Constant::String("index".into()));
2188                    self.chunk.emit_u16(Op::GetProperty, idx_prop, self.line);
2189                    let case_i = self.chunk.add_constant(Constant::Int(i as i64));
2190                    self.chunk.emit_u16(Op::Constant, case_i, self.line);
2191                    self.chunk.emit(Op::Equal, self.line);
2192                    let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2193                    self.chunk.emit(Op::Pop, self.line);
2194
2195                    // Bind variable = result.value
2196                    let get_r2 = self
2197                        .chunk
2198                        .add_constant(Constant::String(result_name.clone()));
2199                    self.chunk.emit_u16(Op::GetVar, get_r2, self.line);
2200                    let val_prop = self.chunk.add_constant(Constant::String("value".into()));
2201                    self.chunk.emit_u16(Op::GetProperty, val_prop, self.line);
2202                    let var_idx = self
2203                        .chunk
2204                        .add_constant(Constant::String(case.variable.clone()));
2205                    self.chunk.emit_u16(Op::DefLet, var_idx, self.line);
2206
2207                    self.compile_try_body(&case.body)?;
2208                    end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
2209                    self.chunk.patch_jump(skip);
2210                    self.chunk.emit(Op::Pop, self.line);
2211                }
2212
2213                // Timeout/default fallthrough (index == -1)
2214                if let Some((_, ref timeout_body)) = timeout {
2215                    self.compile_try_body(timeout_body)?;
2216                } else if let Some(ref def_body) = default_body {
2217                    self.compile_try_body(def_body)?;
2218                } else {
2219                    self.chunk.emit(Op::Nil, self.line);
2220                }
2221
2222                for ej in end_jumps {
2223                    self.chunk.patch_jump(ej);
2224                }
2225            }
2226            Node::Spread(_) => {
2227                return Err(CompileError {
2228                    message: "spread (...) can only be used inside list literals, dict literals, or function call arguments".into(),
2229                    line: self.line,
2230                });
2231            }
2232        }
2233        Ok(())
2234    }
2235
2236    /// Compile a destructuring binding pattern.
2237    /// Expects the RHS value to already be on the stack.
2238    /// After this, the value is consumed (popped) and each binding is defined.
2239    fn compile_destructuring(
2240        &mut self,
2241        pattern: &BindingPattern,
2242        is_mutable: bool,
2243    ) -> Result<(), CompileError> {
2244        let def_op = if is_mutable { Op::DefVar } else { Op::DefLet };
2245        match pattern {
2246            BindingPattern::Identifier(name) => {
2247                // Simple case: just define the variable
2248                let idx = self.chunk.add_constant(Constant::String(name.clone()));
2249                self.chunk.emit_u16(def_op, idx, self.line);
2250            }
2251            BindingPattern::Dict(fields) => {
2252                // Stack has the dict value.
2253                // Emit runtime type check: __assert_dict(value)
2254                self.chunk.emit(Op::Dup, self.line);
2255                let assert_idx = self
2256                    .chunk
2257                    .add_constant(Constant::String("__assert_dict".into()));
2258                self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
2259                self.chunk.emit(Op::Swap, self.line);
2260                self.chunk.emit_u8(Op::Call, 1, self.line);
2261                self.chunk.emit(Op::Pop, self.line); // discard nil result
2262
2263                // For each non-rest field: dup dict, push key string, subscript, define var.
2264                // For rest field: dup dict, call __dict_rest builtin.
2265                let non_rest: Vec<_> = fields.iter().filter(|f| !f.is_rest).collect();
2266                let rest_field = fields.iter().find(|f| f.is_rest);
2267
2268                for field in &non_rest {
2269                    self.chunk.emit(Op::Dup, self.line);
2270                    let key_idx = self.chunk.add_constant(Constant::String(field.key.clone()));
2271                    self.chunk.emit_u16(Op::Constant, key_idx, self.line);
2272                    self.chunk.emit(Op::Subscript, self.line);
2273                    let binding_name = field.alias.as_deref().unwrap_or(&field.key);
2274                    let name_idx = self
2275                        .chunk
2276                        .add_constant(Constant::String(binding_name.to_string()));
2277                    self.chunk.emit_u16(def_op, name_idx, self.line);
2278                }
2279
2280                if let Some(rest) = rest_field {
2281                    // Call the __dict_rest builtin: __dict_rest(dict, [keys_to_exclude])
2282                    // Push function name
2283                    let fn_idx = self
2284                        .chunk
2285                        .add_constant(Constant::String("__dict_rest".into()));
2286                    self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
2287                    // Swap so dict is above function name: [fn, dict]
2288                    self.chunk.emit(Op::Swap, self.line);
2289                    // Build the exclusion keys list
2290                    for field in &non_rest {
2291                        let key_idx = self.chunk.add_constant(Constant::String(field.key.clone()));
2292                        self.chunk.emit_u16(Op::Constant, key_idx, self.line);
2293                    }
2294                    self.chunk
2295                        .emit_u16(Op::BuildList, non_rest.len() as u16, self.line);
2296                    // Call __dict_rest(dict, keys_list) — 2 args
2297                    self.chunk.emit_u8(Op::Call, 2, self.line);
2298                    let rest_name = &rest.key;
2299                    let rest_idx = self.chunk.add_constant(Constant::String(rest_name.clone()));
2300                    self.chunk.emit_u16(def_op, rest_idx, self.line);
2301                } else {
2302                    // Pop the source dict
2303                    self.chunk.emit(Op::Pop, self.line);
2304                }
2305            }
2306            BindingPattern::List(elements) => {
2307                // Stack has the list value.
2308                // Emit runtime type check: __assert_list(value)
2309                self.chunk.emit(Op::Dup, self.line);
2310                let assert_idx = self
2311                    .chunk
2312                    .add_constant(Constant::String("__assert_list".into()));
2313                self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
2314                self.chunk.emit(Op::Swap, self.line);
2315                self.chunk.emit_u8(Op::Call, 1, self.line);
2316                self.chunk.emit(Op::Pop, self.line); // discard nil result
2317
2318                let non_rest: Vec<_> = elements.iter().filter(|e| !e.is_rest).collect();
2319                let rest_elem = elements.iter().find(|e| e.is_rest);
2320
2321                for (i, elem) in non_rest.iter().enumerate() {
2322                    self.chunk.emit(Op::Dup, self.line);
2323                    let idx_const = self.chunk.add_constant(Constant::Int(i as i64));
2324                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
2325                    self.chunk.emit(Op::Subscript, self.line);
2326                    let name_idx = self.chunk.add_constant(Constant::String(elem.name.clone()));
2327                    self.chunk.emit_u16(def_op, name_idx, self.line);
2328                }
2329
2330                if let Some(rest) = rest_elem {
2331                    // Slice the list from index non_rest.len() to end: list[n..]
2332                    // Slice op takes: object, start, end on stack
2333                    // self.chunk.emit(Op::Dup, self.line); -- list is still on stack
2334                    let start_idx = self
2335                        .chunk
2336                        .add_constant(Constant::Int(non_rest.len() as i64));
2337                    self.chunk.emit_u16(Op::Constant, start_idx, self.line);
2338                    self.chunk.emit(Op::Nil, self.line); // end = nil (to end)
2339                    self.chunk.emit(Op::Slice, self.line);
2340                    let rest_name_idx =
2341                        self.chunk.add_constant(Constant::String(rest.name.clone()));
2342                    self.chunk.emit_u16(def_op, rest_name_idx, self.line);
2343                } else {
2344                    // Pop the source list
2345                    self.chunk.emit(Op::Pop, self.line);
2346                }
2347            }
2348        }
2349        Ok(())
2350    }
2351
2352    /// Check if a node produces a value on the stack that needs to be popped.
2353    fn produces_value(node: &Node) -> bool {
2354        match node {
2355            // These nodes do NOT produce a value on the stack
2356            Node::LetBinding { .. }
2357            | Node::VarBinding { .. }
2358            | Node::Assignment { .. }
2359            | Node::ReturnStmt { .. }
2360            | Node::FnDecl { .. }
2361            | Node::ImplBlock { .. }
2362            | Node::StructDecl { .. }
2363            | Node::ThrowStmt { .. }
2364            | Node::BreakStmt
2365            | Node::ContinueStmt => false,
2366            // These compound nodes explicitly produce a value
2367            Node::TryCatch { .. }
2368            | Node::TryExpr { .. }
2369            | Node::Retry { .. }
2370            | Node::GuardStmt { .. }
2371            | Node::DeadlineBlock { .. }
2372            | Node::MutexBlock { .. }
2373            | Node::Spread(_) => true,
2374            // All other expressions produce values
2375            _ => true,
2376        }
2377    }
2378}
2379
2380impl Compiler {
2381    /// Compile a function body into a CompiledFunction (for import support).
2382    pub fn compile_fn_body(
2383        &mut self,
2384        params: &[TypedParam],
2385        body: &[SNode],
2386    ) -> Result<CompiledFunction, CompileError> {
2387        let mut fn_compiler = Compiler::new();
2388        fn_compiler.compile_block(body)?;
2389        fn_compiler.chunk.emit(Op::Nil, 0);
2390        fn_compiler.chunk.emit(Op::Return, 0);
2391        Ok(CompiledFunction {
2392            name: String::new(),
2393            params: TypedParam::names(params),
2394            default_start: TypedParam::default_start(params),
2395            chunk: fn_compiler.chunk,
2396        })
2397    }
2398
2399    /// Compile a match arm body, ensuring it always pushes exactly one value.
2400    fn compile_match_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
2401        if body.is_empty() {
2402            self.chunk.emit(Op::Nil, self.line);
2403        } else {
2404            self.compile_block(body)?;
2405            // If the last statement doesn't produce a value, push nil
2406            if !Self::produces_value(&body.last().unwrap().node) {
2407                self.chunk.emit(Op::Nil, self.line);
2408            }
2409        }
2410        Ok(())
2411    }
2412
2413    /// Emit the binary op instruction for a compound assignment operator.
2414    fn emit_compound_op(&mut self, op: &str) -> Result<(), CompileError> {
2415        match op {
2416            "+" => self.chunk.emit(Op::Add, self.line),
2417            "-" => self.chunk.emit(Op::Sub, self.line),
2418            "*" => self.chunk.emit(Op::Mul, self.line),
2419            "/" => self.chunk.emit(Op::Div, self.line),
2420            "%" => self.chunk.emit(Op::Mod, self.line),
2421            _ => {
2422                return Err(CompileError {
2423                    message: format!("Unknown compound operator: {op}"),
2424                    line: self.line,
2425                })
2426            }
2427        }
2428        Ok(())
2429    }
2430
2431    /// Extract the root variable name from a (possibly nested) access expression.
2432    fn root_var_name(&self, node: &SNode) -> Option<String> {
2433        match &node.node {
2434            Node::Identifier(name) => Some(name.clone()),
2435            Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
2436                self.root_var_name(object)
2437            }
2438            Node::SubscriptAccess { object, .. } => self.root_var_name(object),
2439            _ => None,
2440        }
2441    }
2442}
2443
2444impl Compiler {
2445    /// Recursively collect all enum type names from the AST.
2446    fn collect_enum_names(nodes: &[SNode], names: &mut std::collections::HashSet<String>) {
2447        for sn in nodes {
2448            match &sn.node {
2449                Node::EnumDecl { name, .. } => {
2450                    names.insert(name.clone());
2451                }
2452                Node::Pipeline { body, .. } => {
2453                    Self::collect_enum_names(body, names);
2454                }
2455                Node::FnDecl { body, .. } => {
2456                    Self::collect_enum_names(body, names);
2457                }
2458                Node::Block(stmts) => {
2459                    Self::collect_enum_names(stmts, names);
2460                }
2461                _ => {}
2462            }
2463        }
2464    }
2465
2466    fn collect_interface_methods(
2467        nodes: &[SNode],
2468        interfaces: &mut std::collections::HashMap<String, Vec<String>>,
2469    ) {
2470        for sn in nodes {
2471            match &sn.node {
2472                Node::InterfaceDecl { name, methods } => {
2473                    let method_names: Vec<String> =
2474                        methods.iter().map(|m| m.name.clone()).collect();
2475                    interfaces.insert(name.clone(), method_names);
2476                }
2477                Node::Pipeline { body, .. } | Node::FnDecl { body, .. } => {
2478                    Self::collect_interface_methods(body, interfaces);
2479                }
2480                Node::Block(stmts) => {
2481                    Self::collect_interface_methods(stmts, interfaces);
2482                }
2483                _ => {}
2484            }
2485        }
2486    }
2487}
2488
2489impl Default for Compiler {
2490    fn default() -> Self {
2491        Self::new()
2492    }
2493}
2494
2495/// Check if an AST node contains `_` identifier (pipe placeholder).
2496fn contains_pipe_placeholder(node: &SNode) -> bool {
2497    match &node.node {
2498        Node::Identifier(name) if name == "_" => true,
2499        Node::FunctionCall { args, .. } => args.iter().any(contains_pipe_placeholder),
2500        Node::MethodCall { object, args, .. } => {
2501            contains_pipe_placeholder(object) || args.iter().any(contains_pipe_placeholder)
2502        }
2503        Node::BinaryOp { left, right, .. } => {
2504            contains_pipe_placeholder(left) || contains_pipe_placeholder(right)
2505        }
2506        Node::UnaryOp { operand, .. } => contains_pipe_placeholder(operand),
2507        Node::ListLiteral(items) => items.iter().any(contains_pipe_placeholder),
2508        Node::PropertyAccess { object, .. } => contains_pipe_placeholder(object),
2509        Node::SubscriptAccess { object, index } => {
2510            contains_pipe_placeholder(object) || contains_pipe_placeholder(index)
2511        }
2512        _ => false,
2513    }
2514}
2515
2516/// Replace all `_` identifiers with `__pipe` in an AST node (for pipe placeholder desugaring).
2517fn replace_pipe_placeholder(node: &SNode) -> SNode {
2518    let new_node = match &node.node {
2519        Node::Identifier(name) if name == "_" => Node::Identifier("__pipe".into()),
2520        Node::FunctionCall { name, args } => Node::FunctionCall {
2521            name: name.clone(),
2522            args: args.iter().map(replace_pipe_placeholder).collect(),
2523        },
2524        Node::MethodCall {
2525            object,
2526            method,
2527            args,
2528        } => Node::MethodCall {
2529            object: Box::new(replace_pipe_placeholder(object)),
2530            method: method.clone(),
2531            args: args.iter().map(replace_pipe_placeholder).collect(),
2532        },
2533        Node::BinaryOp { op, left, right } => Node::BinaryOp {
2534            op: op.clone(),
2535            left: Box::new(replace_pipe_placeholder(left)),
2536            right: Box::new(replace_pipe_placeholder(right)),
2537        },
2538        Node::UnaryOp { op, operand } => Node::UnaryOp {
2539            op: op.clone(),
2540            operand: Box::new(replace_pipe_placeholder(operand)),
2541        },
2542        Node::ListLiteral(items) => {
2543            Node::ListLiteral(items.iter().map(replace_pipe_placeholder).collect())
2544        }
2545        Node::PropertyAccess { object, property } => Node::PropertyAccess {
2546            object: Box::new(replace_pipe_placeholder(object)),
2547            property: property.clone(),
2548        },
2549        Node::SubscriptAccess { object, index } => Node::SubscriptAccess {
2550            object: Box::new(replace_pipe_placeholder(object)),
2551            index: Box::new(replace_pipe_placeholder(index)),
2552        },
2553        _ => return node.clone(),
2554    };
2555    SNode::new(new_node, node.span)
2556}
2557
2558#[cfg(test)]
2559mod tests {
2560    use super::*;
2561    use harn_lexer::Lexer;
2562    use harn_parser::Parser;
2563
2564    fn compile_source(source: &str) -> Chunk {
2565        let mut lexer = Lexer::new(source);
2566        let tokens = lexer.tokenize().unwrap();
2567        let mut parser = Parser::new(tokens);
2568        let program = parser.parse().unwrap();
2569        Compiler::new().compile(&program).unwrap()
2570    }
2571
2572    #[test]
2573    fn test_compile_arithmetic() {
2574        let chunk = compile_source("pipeline test(task) { let x = 2 + 3 }");
2575        assert!(!chunk.code.is_empty());
2576        // Should have constants: 2, 3, "x"
2577        assert!(chunk.constants.contains(&Constant::Int(2)));
2578        assert!(chunk.constants.contains(&Constant::Int(3)));
2579    }
2580
2581    #[test]
2582    fn test_compile_function_call() {
2583        let chunk = compile_source("pipeline test(task) { log(42) }");
2584        let disasm = chunk.disassemble("test");
2585        assert!(disasm.contains("CALL"));
2586    }
2587
2588    #[test]
2589    fn test_compile_if_else() {
2590        let chunk =
2591            compile_source(r#"pipeline test(task) { if true { log("yes") } else { log("no") } }"#);
2592        let disasm = chunk.disassemble("test");
2593        assert!(disasm.contains("JUMP_IF_FALSE"));
2594        assert!(disasm.contains("JUMP"));
2595    }
2596
2597    #[test]
2598    fn test_compile_while() {
2599        let chunk = compile_source("pipeline test(task) { var i = 0\n while i < 5 { i = i + 1 } }");
2600        let disasm = chunk.disassemble("test");
2601        assert!(disasm.contains("JUMP_IF_FALSE"));
2602        // Should have a backward jump
2603        assert!(disasm.contains("JUMP"));
2604    }
2605
2606    #[test]
2607    fn test_compile_closure() {
2608        let chunk = compile_source("pipeline test(task) { let f = { x -> x * 2 } }");
2609        assert!(!chunk.functions.is_empty());
2610        assert_eq!(chunk.functions[0].params, vec!["x"]);
2611    }
2612
2613    #[test]
2614    fn test_compile_list() {
2615        let chunk = compile_source("pipeline test(task) { let a = [1, 2, 3] }");
2616        let disasm = chunk.disassemble("test");
2617        assert!(disasm.contains("BUILD_LIST"));
2618    }
2619
2620    #[test]
2621    fn test_compile_dict() {
2622        let chunk = compile_source(r#"pipeline test(task) { let d = {name: "test"} }"#);
2623        let disasm = chunk.disassemble("test");
2624        assert!(disasm.contains("BUILD_DICT"));
2625    }
2626
2627    #[test]
2628    fn test_disassemble() {
2629        let chunk = compile_source("pipeline test(task) { log(2 + 3) }");
2630        let disasm = chunk.disassemble("test");
2631        // Should be readable
2632        assert!(disasm.contains("CONSTANT"));
2633        assert!(disasm.contains("ADD"));
2634        assert!(disasm.contains("CALL"));
2635    }
2636}