Skip to main content

harn_vm/
compiler.rs

1use std::collections::BTreeMap;
2use std::rc::Rc;
3
4use harn_lexer::StringSegment;
5use harn_parser::{BindingPattern, Node, ParallelMode, SNode, ShapeField, TypeExpr, TypedParam};
6
7use crate::chunk::{Chunk, CompiledFunction, Constant, Op};
8use crate::schema;
9use crate::value::VmValue;
10
11/// Compile error.
12#[derive(Debug)]
13pub struct CompileError {
14    pub message: String,
15    pub line: u32,
16}
17
18impl std::fmt::Display for CompileError {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        write!(f, "Compile error at line {}: {}", self.line, self.message)
21    }
22}
23
24impl std::error::Error for CompileError {}
25
26/// Entry in the compiler's pending-finally stack. See the field-level doc on
27/// `Compiler::finally_bodies` for the unwind semantics each variant encodes.
28#[derive(Clone, Debug)]
29enum FinallyEntry {
30    Finally(Vec<SNode>),
31    CatchBarrier,
32}
33
34/// Tracks loop context for break/continue compilation.
35struct LoopContext {
36    /// Offset of the loop start (for continue).
37    start_offset: usize,
38    /// Positions of break jumps that need patching to the loop end.
39    break_patches: Vec<usize>,
40    /// True if this is a for-in loop (has an iterator to clean up on break).
41    has_iterator: bool,
42    /// Number of exception handlers active at loop entry.
43    handler_depth: usize,
44    /// Number of pending finally bodies at loop entry.
45    finally_depth: usize,
46    /// Lexical scope depth at loop entry.
47    scope_depth: usize,
48}
49
50/// Compiles an AST into bytecode.
51pub struct Compiler {
52    chunk: Chunk,
53    line: u32,
54    column: u32,
55    /// Track enum type names so PropertyAccess on them can produce EnumVariant.
56    enum_names: std::collections::HashSet<String>,
57    /// Track interface names → method names for runtime enforcement.
58    interface_methods: std::collections::HashMap<String, Vec<String>>,
59    /// Stack of active loop contexts for break/continue.
60    loop_stack: Vec<LoopContext>,
61    /// Current depth of exception handlers (for cleanup on break/continue).
62    handler_depth: usize,
63    /// Stack of pending finally bodies plus catch-handler barriers for
64    /// unwind-aware lowering of `throw`, `return`, `break`, and `continue`.
65    ///
66    /// A `Finally` entry is a pending finally body that must execute when
67    /// control exits its enclosing try block. A `CatchBarrier` marks the
68    /// boundary of an active `try/catch` handler: throws emitted inside
69    /// the try body are caught locally, so pre-running finallys *beyond*
70    /// the barrier would wrongly fire side effects for outer blocks the
71    /// throw never actually escapes. Throw lowering stops at the innermost
72    /// barrier; `return`/`break`/`continue`, which do transfer past local
73    /// handlers, still run every pending `Finally` up to their target.
74    finally_bodies: Vec<FinallyEntry>,
75    /// Counter for unique temp variable names.
76    temp_counter: usize,
77    /// Number of lexical block scopes currently active in this compiled frame.
78    scope_depth: usize,
79    /// Top-level `type` aliases, used to lower `schema_of(T)` and
80    /// `output_schema: T` into constant JSON-Schema dicts at compile time.
81    type_aliases: std::collections::HashMap<String, TypeExpr>,
82    /// True when this compiler is emitting code outside any function-like
83    /// scope (module top-level statements). `try*` is rejected here
84    /// because the rethrow has no enclosing function to live in.
85    /// Pipeline bodies and nested `Compiler::new()` instances (fn,
86    /// closure, tool, etc.) flip this to false before compiling.
87    module_level: bool,
88}
89
90impl Compiler {
91    pub fn new() -> Self {
92        Self {
93            chunk: Chunk::new(),
94            line: 1,
95            column: 1,
96            enum_names: std::collections::HashSet::new(),
97            interface_methods: std::collections::HashMap::new(),
98            loop_stack: Vec::new(),
99            handler_depth: 0,
100            finally_bodies: Vec::new(),
101            temp_counter: 0,
102            scope_depth: 0,
103            type_aliases: std::collections::HashMap::new(),
104            module_level: true,
105        }
106    }
107
108    /// Compiler instance for a nested function-like body (fn, closure,
109    /// tool, parallel arm, etc.). Differs from `new()` only in that
110    /// `module_level` starts false — `try*` is allowed inside.
111    fn for_nested_body() -> Self {
112        let mut c = Self::new();
113        c.module_level = false;
114        c
115    }
116
117    /// Populate `type_aliases` from a program's top-level `type T = ...`
118    /// declarations so later lowerings can resolve alias names to their
119    /// canonical `TypeExpr`.
120    fn collect_type_aliases(&mut self, program: &[SNode]) {
121        for sn in program {
122            if let Node::TypeDecl {
123                name,
124                type_expr,
125                type_params: _,
126            } = &sn.node
127            {
128                self.type_aliases.insert(name.clone(), type_expr.clone());
129            }
130        }
131    }
132
133    /// Expand a single layer of alias references. Returns the resolved
134    /// `TypeExpr` with all `Named(T)` nodes whose `T` is a known alias
135    /// replaced by the alias's body.
136    fn expand_alias(&self, ty: &TypeExpr) -> TypeExpr {
137        match ty {
138            TypeExpr::Named(name) => {
139                if let Some(target) = self.type_aliases.get(name) {
140                    self.expand_alias(target)
141                } else {
142                    TypeExpr::Named(name.clone())
143                }
144            }
145            TypeExpr::Union(types) => {
146                TypeExpr::Union(types.iter().map(|t| self.expand_alias(t)).collect())
147            }
148            TypeExpr::Shape(fields) => TypeExpr::Shape(
149                fields
150                    .iter()
151                    .map(|field| ShapeField {
152                        name: field.name.clone(),
153                        type_expr: self.expand_alias(&field.type_expr),
154                        optional: field.optional,
155                    })
156                    .collect(),
157            ),
158            TypeExpr::List(inner) => TypeExpr::List(Box::new(self.expand_alias(inner))),
159            TypeExpr::Iter(inner) => TypeExpr::Iter(Box::new(self.expand_alias(inner))),
160            TypeExpr::DictType(k, v) => TypeExpr::DictType(
161                Box::new(self.expand_alias(k)),
162                Box::new(self.expand_alias(v)),
163            ),
164            TypeExpr::FnType {
165                params,
166                return_type,
167            } => TypeExpr::FnType {
168                params: params.iter().map(|p| self.expand_alias(p)).collect(),
169                return_type: Box::new(self.expand_alias(return_type)),
170            },
171            TypeExpr::Applied { name, args } => TypeExpr::Applied {
172                name: name.clone(),
173                args: args.iter().map(|a| self.expand_alias(a)).collect(),
174            },
175            TypeExpr::Never => TypeExpr::Never,
176            TypeExpr::LitString(s) => TypeExpr::LitString(s.clone()),
177            TypeExpr::LitInt(v) => TypeExpr::LitInt(*v),
178        }
179    }
180
181    /// Build the JSON-Schema VmValue for a named type alias, or `None` if
182    /// the name is unknown or the alias cannot be lowered to a schema.
183    fn schema_value_for_alias(&self, name: &str) -> Option<VmValue> {
184        let ty = self.type_aliases.get(name)?;
185        let expanded = self.expand_alias(ty);
186        Self::type_expr_to_schema_value(&expanded)
187    }
188
189    /// Schema-guard builtins that accept a schema as their second argument.
190    /// When callers pass a type-alias identifier here, the compiler lowers
191    /// it to the alias's JSON-Schema dict constant.
192    fn is_schema_guard(name: &str) -> bool {
193        matches!(
194            name,
195            "schema_is"
196                | "schema_expect"
197                | "schema_parse"
198                | "schema_check"
199                | "is_type"
200                | "json_validate"
201        )
202    }
203
204    /// Check whether a dict-literal key node matches the given keyword
205    /// (identifier or string literal form).
206    fn entry_key_is(key: &SNode, keyword: &str) -> bool {
207        matches!(
208            &key.node,
209            Node::Identifier(name) | Node::StringLiteral(name) | Node::RawStringLiteral(name)
210                if name == keyword
211        )
212    }
213
214    /// Compile a program (list of top-level nodes) into a Chunk.
215    /// Finds the entry pipeline and compiles its body, including inherited bodies.
216    pub fn compile(mut self, program: &[SNode]) -> Result<Chunk, CompileError> {
217        // Pre-scan so we can recognize EnumName.Variant as enum construction
218        // even when the enum is declared inside a pipeline.
219        Self::collect_enum_names(program, &mut self.enum_names);
220        self.enum_names.insert("Result".to_string());
221        Self::collect_interface_methods(program, &mut self.interface_methods);
222        self.collect_type_aliases(program);
223
224        for sn in program {
225            match &sn.node {
226                Node::ImportDecl { .. } | Node::SelectiveImport { .. } => {
227                    self.compile_node(sn)?;
228                }
229                _ => {}
230            }
231        }
232        let main = program
233            .iter()
234            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == "default"))
235            .or_else(|| {
236                program
237                    .iter()
238                    .find(|sn| matches!(&sn.node, Node::Pipeline { .. }))
239            });
240
241        if let Some(sn) = main {
242            self.compile_top_level_declarations(program)?;
243            if let Node::Pipeline { body, extends, .. } = &sn.node {
244                if let Some(parent_name) = extends {
245                    self.compile_parent_pipeline(program, parent_name)?;
246                }
247                let saved = std::mem::replace(&mut self.module_level, false);
248                self.compile_block(body)?;
249                self.module_level = saved;
250            }
251        } else {
252            // Script mode: no pipeline found, treat top-level as implicit entry.
253            let top_level: Vec<&SNode> = program
254                .iter()
255                .filter(|sn| {
256                    !matches!(
257                        &sn.node,
258                        Node::ImportDecl { .. } | Node::SelectiveImport { .. }
259                    )
260                })
261                .collect();
262            for sn in &top_level {
263                self.compile_node(sn)?;
264                if Self::produces_value(&sn.node) {
265                    self.chunk.emit(Op::Pop, self.line);
266                }
267            }
268        }
269
270        for fb in self.all_pending_finallys() {
271            self.compile_finally_inline(&fb)?;
272        }
273        self.chunk.emit(Op::Nil, self.line);
274        self.chunk.emit(Op::Return, self.line);
275        Ok(self.chunk)
276    }
277
278    /// Compile a specific named pipeline (for test runners).
279    pub fn compile_named(
280        mut self,
281        program: &[SNode],
282        pipeline_name: &str,
283    ) -> Result<Chunk, CompileError> {
284        Self::collect_enum_names(program, &mut self.enum_names);
285        Self::collect_interface_methods(program, &mut self.interface_methods);
286        self.collect_type_aliases(program);
287
288        for sn in program {
289            if matches!(
290                &sn.node,
291                Node::ImportDecl { .. } | Node::SelectiveImport { .. }
292            ) {
293                self.compile_node(sn)?;
294            }
295        }
296        let target = program
297            .iter()
298            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == pipeline_name));
299
300        if let Some(sn) = target {
301            self.compile_top_level_declarations(program)?;
302            if let Node::Pipeline { body, extends, .. } = &sn.node {
303                if let Some(parent_name) = extends {
304                    self.compile_parent_pipeline(program, parent_name)?;
305                }
306                let saved = std::mem::replace(&mut self.module_level, false);
307                self.compile_block(body)?;
308                self.module_level = saved;
309            }
310        }
311
312        for fb in self.all_pending_finallys() {
313            self.compile_finally_inline(&fb)?;
314        }
315        self.chunk.emit(Op::Nil, self.line);
316        self.chunk.emit(Op::Return, self.line);
317        Ok(self.chunk)
318    }
319
320    /// Recursively compile parent pipeline bodies (for extends).
321    fn compile_parent_pipeline(
322        &mut self,
323        program: &[SNode],
324        parent_name: &str,
325    ) -> Result<(), CompileError> {
326        let parent = program
327            .iter()
328            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == parent_name));
329        if let Some(sn) = parent {
330            if let Node::Pipeline { body, extends, .. } = &sn.node {
331                if let Some(grandparent) = extends {
332                    self.compile_parent_pipeline(program, grandparent)?;
333                }
334                for stmt in body {
335                    self.compile_node(stmt)?;
336                    if Self::produces_value(&stmt.node) {
337                        self.chunk.emit(Op::Pop, self.line);
338                    }
339                }
340            }
341        }
342        Ok(())
343    }
344
345    /// Emit bytecode preamble for default parameter values.
346    /// For each param with a default at index i, emits:
347    ///   GetArgc; PushInt (i+1); GreaterEqual; JumpIfTrue <skip>;
348    ///   [compile default expr]; DefLet param_name; <skip>:
349    fn emit_default_preamble(&mut self, params: &[TypedParam]) -> Result<(), CompileError> {
350        for (i, param) in params.iter().enumerate() {
351            if let Some(default_expr) = &param.default_value {
352                self.chunk.emit(Op::GetArgc, self.line);
353                let threshold_idx = self.chunk.add_constant(Constant::Int((i + 1) as i64));
354                self.chunk.emit_u16(Op::Constant, threshold_idx, self.line);
355                self.chunk.emit(Op::GreaterEqual, self.line);
356                let skip_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
357                // JumpIfTrue doesn't pop its boolean operand.
358                self.chunk.emit(Op::Pop, self.line);
359                self.compile_node(default_expr)?;
360                let name_idx = self
361                    .chunk
362                    .add_constant(Constant::String(param.name.clone()));
363                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
364                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
365                self.chunk.patch_jump(skip_jump);
366                self.chunk.emit(Op::Pop, self.line);
367                self.chunk.patch_jump(end_jump);
368            }
369        }
370        Ok(())
371    }
372
373    /// Emit runtime type checks for parameters with type annotations.
374    /// Interface types keep their dedicated runtime guard; all other supported
375    /// runtime-checkable types compile to a schema literal and call
376    /// `__assert_schema(value, param_name, schema)`.
377    fn emit_type_checks(&mut self, params: &[TypedParam]) {
378        for param in params {
379            if let Some(type_expr) = &param.type_expr {
380                if let harn_parser::TypeExpr::Named(name) = type_expr {
381                    if let Some(methods) = self.interface_methods.get(name) {
382                        let fn_idx = self
383                            .chunk
384                            .add_constant(Constant::String("__assert_interface".into()));
385                        self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
386                        let var_idx = self
387                            .chunk
388                            .add_constant(Constant::String(param.name.clone()));
389                        self.chunk.emit_u16(Op::GetVar, var_idx, self.line);
390                        let name_idx = self
391                            .chunk
392                            .add_constant(Constant::String(param.name.clone()));
393                        self.chunk.emit_u16(Op::Constant, name_idx, self.line);
394                        let iface_idx = self.chunk.add_constant(Constant::String(name.clone()));
395                        self.chunk.emit_u16(Op::Constant, iface_idx, self.line);
396                        let methods_str = methods.join(",");
397                        let methods_idx = self.chunk.add_constant(Constant::String(methods_str));
398                        self.chunk.emit_u16(Op::Constant, methods_idx, self.line);
399                        self.chunk.emit_u8(Op::Call, 4, self.line);
400                        self.chunk.emit(Op::Pop, self.line);
401                        continue;
402                    }
403                }
404
405                if let Some(schema) = Self::type_expr_to_schema_value(type_expr) {
406                    let fn_idx = self
407                        .chunk
408                        .add_constant(Constant::String("__assert_schema".into()));
409                    self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
410                    let var_idx = self
411                        .chunk
412                        .add_constant(Constant::String(param.name.clone()));
413                    self.chunk.emit_u16(Op::GetVar, var_idx, self.line);
414                    let name_idx = self
415                        .chunk
416                        .add_constant(Constant::String(param.name.clone()));
417                    self.chunk.emit_u16(Op::Constant, name_idx, self.line);
418                    self.emit_vm_value_literal(&schema);
419                    self.chunk.emit_u8(Op::Call, 3, self.line);
420                    self.chunk.emit(Op::Pop, self.line);
421                }
422            }
423        }
424    }
425
426    fn type_expr_to_schema_value(type_expr: &harn_parser::TypeExpr) -> Option<VmValue> {
427        match type_expr {
428            harn_parser::TypeExpr::Named(name) => match name.as_str() {
429                "int" | "float" | "string" | "bool" | "list" | "dict" | "set" | "nil"
430                | "closure" => Some(VmValue::Dict(Rc::new(BTreeMap::from([(
431                    "type".to_string(),
432                    VmValue::String(Rc::from(name.as_str())),
433                )])))),
434                _ => None,
435            },
436            harn_parser::TypeExpr::Shape(fields) => {
437                let mut properties = BTreeMap::new();
438                let mut required = Vec::new();
439                for field in fields {
440                    let field_schema = Self::type_expr_to_schema_value(&field.type_expr)?;
441                    properties.insert(field.name.clone(), field_schema);
442                    if !field.optional {
443                        required.push(VmValue::String(Rc::from(field.name.as_str())));
444                    }
445                }
446                let mut out = BTreeMap::new();
447                out.insert("type".to_string(), VmValue::String(Rc::from("dict")));
448                out.insert("properties".to_string(), VmValue::Dict(Rc::new(properties)));
449                if !required.is_empty() {
450                    out.insert("required".to_string(), VmValue::List(Rc::new(required)));
451                }
452                Some(VmValue::Dict(Rc::new(out)))
453            }
454            harn_parser::TypeExpr::List(inner) => {
455                let mut out = BTreeMap::new();
456                out.insert("type".to_string(), VmValue::String(Rc::from("list")));
457                if let Some(item_schema) = Self::type_expr_to_schema_value(inner) {
458                    out.insert("items".to_string(), item_schema);
459                }
460                Some(VmValue::Dict(Rc::new(out)))
461            }
462            harn_parser::TypeExpr::DictType(key, value) => {
463                let mut out = BTreeMap::new();
464                out.insert("type".to_string(), VmValue::String(Rc::from("dict")));
465                if matches!(key.as_ref(), harn_parser::TypeExpr::Named(name) if name == "string") {
466                    if let Some(value_schema) = Self::type_expr_to_schema_value(value) {
467                        out.insert("additional_properties".to_string(), value_schema);
468                    }
469                }
470                Some(VmValue::Dict(Rc::new(out)))
471            }
472            harn_parser::TypeExpr::Union(members) => {
473                // Special-case unions of literals: emit as `enum: [...]`
474                // so the schema round-trips as canonical JSON Schema and
475                // is ACP-/OpenAPI-compatible. Mixed unions fall back to
476                // the `union:` key that validators recognize.
477                if !members.is_empty()
478                    && members
479                        .iter()
480                        .all(|m| matches!(m, harn_parser::TypeExpr::LitString(_)))
481                {
482                    let values = members
483                        .iter()
484                        .map(|m| match m {
485                            harn_parser::TypeExpr::LitString(s) => {
486                                VmValue::String(Rc::from(s.as_str()))
487                            }
488                            _ => unreachable!(),
489                        })
490                        .collect::<Vec<_>>();
491                    return Some(VmValue::Dict(Rc::new(BTreeMap::from([
492                        ("type".to_string(), VmValue::String(Rc::from("string"))),
493                        ("enum".to_string(), VmValue::List(Rc::new(values))),
494                    ]))));
495                }
496                if !members.is_empty()
497                    && members
498                        .iter()
499                        .all(|m| matches!(m, harn_parser::TypeExpr::LitInt(_)))
500                {
501                    let values = members
502                        .iter()
503                        .map(|m| match m {
504                            harn_parser::TypeExpr::LitInt(v) => VmValue::Int(*v),
505                            _ => unreachable!(),
506                        })
507                        .collect::<Vec<_>>();
508                    return Some(VmValue::Dict(Rc::new(BTreeMap::from([
509                        ("type".to_string(), VmValue::String(Rc::from("int"))),
510                        ("enum".to_string(), VmValue::List(Rc::new(values))),
511                    ]))));
512                }
513                let branches = members
514                    .iter()
515                    .filter_map(Self::type_expr_to_schema_value)
516                    .collect::<Vec<_>>();
517                if branches.is_empty() {
518                    None
519                } else {
520                    Some(VmValue::Dict(Rc::new(BTreeMap::from([(
521                        "union".to_string(),
522                        VmValue::List(Rc::new(branches)),
523                    )]))))
524                }
525            }
526            harn_parser::TypeExpr::FnType { .. } => {
527                Some(VmValue::Dict(Rc::new(BTreeMap::from([(
528                    "type".to_string(),
529                    VmValue::String(Rc::from("closure")),
530                )]))))
531            }
532            harn_parser::TypeExpr::Applied { .. } => None,
533            harn_parser::TypeExpr::Iter(_) => None,
534            harn_parser::TypeExpr::Never => None,
535            harn_parser::TypeExpr::LitString(s) => Some(VmValue::Dict(Rc::new(BTreeMap::from([
536                ("type".to_string(), VmValue::String(Rc::from("string"))),
537                ("const".to_string(), VmValue::String(Rc::from(s.as_str()))),
538            ])))),
539            harn_parser::TypeExpr::LitInt(v) => Some(VmValue::Dict(Rc::new(BTreeMap::from([
540                ("type".to_string(), VmValue::String(Rc::from("int"))),
541                ("const".to_string(), VmValue::Int(*v)),
542            ])))),
543        }
544    }
545
546    fn emit_vm_value_literal(&mut self, value: &VmValue) {
547        match value {
548            VmValue::String(text) => {
549                let idx = self.chunk.add_constant(Constant::String(text.to_string()));
550                self.chunk.emit_u16(Op::Constant, idx, self.line);
551            }
552            VmValue::Int(number) => {
553                let idx = self.chunk.add_constant(Constant::Int(*number));
554                self.chunk.emit_u16(Op::Constant, idx, self.line);
555            }
556            VmValue::Float(number) => {
557                let idx = self.chunk.add_constant(Constant::Float(*number));
558                self.chunk.emit_u16(Op::Constant, idx, self.line);
559            }
560            VmValue::Bool(value) => {
561                let idx = self.chunk.add_constant(Constant::Bool(*value));
562                self.chunk.emit_u16(Op::Constant, idx, self.line);
563            }
564            VmValue::Nil => self.chunk.emit(Op::Nil, self.line),
565            VmValue::List(items) => {
566                for item in items.iter() {
567                    self.emit_vm_value_literal(item);
568                }
569                self.chunk
570                    .emit_u16(Op::BuildList, items.len() as u16, self.line);
571            }
572            VmValue::Dict(entries) => {
573                for (key, item) in entries.iter() {
574                    let key_idx = self.chunk.add_constant(Constant::String(key.clone()));
575                    self.chunk.emit_u16(Op::Constant, key_idx, self.line);
576                    self.emit_vm_value_literal(item);
577                }
578                self.chunk
579                    .emit_u16(Op::BuildDict, entries.len() as u16, self.line);
580            }
581            _ => {}
582        }
583    }
584
585    /// Emit the extra u16 type name index after a TryCatchSetup jump.
586    fn emit_type_name_extra(&mut self, type_name_idx: u16) {
587        let hi = (type_name_idx >> 8) as u8;
588        let lo = type_name_idx as u8;
589        self.chunk.code.push(hi);
590        self.chunk.code.push(lo);
591        self.chunk.lines.push(self.line);
592        self.chunk.columns.push(self.column);
593        self.chunk.lines.push(self.line);
594        self.chunk.columns.push(self.column);
595    }
596
597    /// Compile a try/catch body block (produces a value on the stack).
598    fn compile_try_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
599        if body.is_empty() {
600            self.chunk.emit(Op::Nil, self.line);
601        } else {
602            self.compile_scoped_block(body)?;
603        }
604        Ok(())
605    }
606
607    /// Compile catch error binding (error value is on stack from handler).
608    fn compile_catch_binding(&mut self, error_var: &Option<String>) -> Result<(), CompileError> {
609        if let Some(var_name) = error_var {
610            let idx = self.chunk.add_constant(Constant::String(var_name.clone()));
611            self.chunk.emit_u16(Op::DefLet, idx, self.line);
612        } else {
613            self.chunk.emit(Op::Pop, self.line);
614        }
615        Ok(())
616    }
617
618    /// Compile finally body inline, discarding its result value.
619    /// `compile_scoped_block` always leaves exactly one value on the stack
620    /// (Nil for non-value tail statements), so the trailing Pop is
621    /// unconditional — otherwise a finally ending in e.g. `x = x + 1`
622    /// would leave a stray Nil that corrupts the surrounding expression
623    /// when the enclosing try/finally is used in expression position.
624    fn compile_finally_inline(&mut self, finally_body: &[SNode]) -> Result<(), CompileError> {
625        if !finally_body.is_empty() {
626            self.compile_scoped_block(finally_body)?;
627            self.chunk.emit(Op::Pop, self.line);
628        }
629        Ok(())
630    }
631
632    /// Collect pending finally bodies from the top of the stack down to
633    /// (but not including) the innermost `CatchBarrier`. Used by `throw`
634    /// lowering: throws caught locally don't unwind past the catch, so
635    /// finallys behind the barrier aren't on the throw's exit path.
636    fn pending_finallys_until_barrier(&self) -> Vec<Vec<SNode>> {
637        let mut out = Vec::new();
638        for entry in self.finally_bodies.iter().rev() {
639            match entry {
640                FinallyEntry::CatchBarrier => break,
641                FinallyEntry::Finally(body) => out.push(body.clone()),
642            }
643        }
644        out
645    }
646
647    /// Collect every pending finally body from the top of the stack down
648    /// to `floor` (an index produced by `finally_bodies.len()` at some
649    /// earlier point), skipping `CatchBarrier` markers. Used by `return`,
650    /// `break`, and `continue` lowering — they transfer control past local
651    /// handlers, so every `Finally` up to their target must run.
652    fn pending_finallys_down_to(&self, floor: usize) -> Vec<Vec<SNode>> {
653        let mut out = Vec::new();
654        for entry in self.finally_bodies[floor..].iter().rev() {
655            if let FinallyEntry::Finally(body) = entry {
656                out.push(body.clone());
657            }
658        }
659        out
660    }
661
662    /// All pending finally bodies (entire stack), skipping barriers.
663    fn all_pending_finallys(&self) -> Vec<Vec<SNode>> {
664        self.pending_finallys_down_to(0)
665    }
666
667    /// True if there are any pending finally bodies (not just barriers).
668    fn has_pending_finally(&self) -> bool {
669        self.finally_bodies
670            .iter()
671            .any(|e| matches!(e, FinallyEntry::Finally(_)))
672    }
673
674    /// Save a thrown value to a temp and rethrow without running finally.
675    ///
676    /// Historically this helper also invoked `compile_finally_inline` on the
677    /// thrown path, but that produced observable double-runs: the
678    /// `Node::ThrowStmt` lowering (below) already iterates `finally_bodies`
679    /// and runs each pending finally inline *before* emitting `Op::Throw`, so
680    /// a second run here fired the same side effects twice. Finally now runs
681    /// exactly once — via the throw-emit path during unwinding.
682    fn compile_plain_rethrow(&mut self) -> Result<(), CompileError> {
683        self.temp_counter += 1;
684        let temp_name = format!("__finally_err_{}__", self.temp_counter);
685        let err_idx = self.chunk.add_constant(Constant::String(temp_name.clone()));
686        self.chunk.emit_u16(Op::DefVar, err_idx, self.line);
687        let get_idx = self.chunk.add_constant(Constant::String(temp_name));
688        self.chunk.emit_u16(Op::GetVar, get_idx, self.line);
689        self.chunk.emit(Op::Throw, self.line);
690        Ok(())
691    }
692
693    fn begin_scope(&mut self) {
694        self.chunk.emit(Op::PushScope, self.line);
695        self.scope_depth += 1;
696    }
697
698    fn end_scope(&mut self) {
699        if self.scope_depth > 0 {
700            self.chunk.emit(Op::PopScope, self.line);
701            self.scope_depth -= 1;
702        }
703    }
704
705    fn unwind_scopes_to(&mut self, target_depth: usize) {
706        while self.scope_depth > target_depth {
707            self.chunk.emit(Op::PopScope, self.line);
708            self.scope_depth -= 1;
709        }
710    }
711
712    fn compile_scoped_block(&mut self, stmts: &[SNode]) -> Result<(), CompileError> {
713        self.begin_scope();
714        if stmts.is_empty() {
715            self.chunk.emit(Op::Nil, self.line);
716        } else {
717            self.compile_block(stmts)?;
718        }
719        self.end_scope();
720        Ok(())
721    }
722
723    fn compile_scoped_statements(&mut self, stmts: &[SNode]) -> Result<(), CompileError> {
724        self.begin_scope();
725        for sn in stmts {
726            self.compile_node(sn)?;
727            if Self::produces_value(&sn.node) {
728                self.chunk.emit(Op::Pop, self.line);
729            }
730        }
731        self.end_scope();
732        Ok(())
733    }
734
735    fn compile_block(&mut self, stmts: &[SNode]) -> Result<(), CompileError> {
736        for (i, snode) in stmts.iter().enumerate() {
737            self.compile_node(snode)?;
738            let is_last = i == stmts.len() - 1;
739            if is_last {
740                // Ensure the block always leaves exactly one value on the stack.
741                if !Self::produces_value(&snode.node) {
742                    self.chunk.emit(Op::Nil, self.line);
743                }
744            } else {
745                if Self::produces_value(&snode.node) {
746                    self.chunk.emit(Op::Pop, self.line);
747                }
748            }
749        }
750        Ok(())
751    }
752
753    fn compile_node(&mut self, snode: &SNode) -> Result<(), CompileError> {
754        self.line = snode.span.line as u32;
755        self.column = snode.span.column as u32;
756        self.chunk.set_column(self.column);
757        match &snode.node {
758            Node::IntLiteral(n) => {
759                let idx = self.chunk.add_constant(Constant::Int(*n));
760                self.chunk.emit_u16(Op::Constant, idx, self.line);
761            }
762            Node::FloatLiteral(n) => {
763                let idx = self.chunk.add_constant(Constant::Float(*n));
764                self.chunk.emit_u16(Op::Constant, idx, self.line);
765            }
766            Node::StringLiteral(s) | Node::RawStringLiteral(s) => {
767                let idx = self.chunk.add_constant(Constant::String(s.clone()));
768                self.chunk.emit_u16(Op::Constant, idx, self.line);
769            }
770            Node::BoolLiteral(true) => self.chunk.emit(Op::True, self.line),
771            Node::BoolLiteral(false) => self.chunk.emit(Op::False, self.line),
772            Node::NilLiteral => self.chunk.emit(Op::Nil, self.line),
773            Node::DurationLiteral(ms) => {
774                let idx = self.chunk.add_constant(Constant::Duration(*ms));
775                self.chunk.emit_u16(Op::Constant, idx, self.line);
776            }
777
778            Node::Identifier(name) => {
779                let idx = self.chunk.add_constant(Constant::String(name.clone()));
780                self.chunk.emit_u16(Op::GetVar, idx, self.line);
781            }
782
783            Node::LetBinding { pattern, value, .. } => {
784                self.compile_node(value)?;
785                self.compile_destructuring(pattern, false)?;
786            }
787
788            Node::VarBinding { pattern, value, .. } => {
789                self.compile_node(value)?;
790                self.compile_destructuring(pattern, true)?;
791            }
792
793            Node::Assignment {
794                target, value, op, ..
795            } => {
796                if let Node::Identifier(name) = &target.node {
797                    let idx = self.chunk.add_constant(Constant::String(name.clone()));
798                    if let Some(op) = op {
799                        self.chunk.emit_u16(Op::GetVar, idx, self.line);
800                        self.compile_node(value)?;
801                        self.emit_compound_op(op)?;
802                        self.chunk.emit_u16(Op::SetVar, idx, self.line);
803                    } else {
804                        self.compile_node(value)?;
805                        self.chunk.emit_u16(Op::SetVar, idx, self.line);
806                    }
807                } else if let Node::PropertyAccess { object, property } = &target.node {
808                    if let Some(var_name) = self.root_var_name(object) {
809                        let var_idx = self.chunk.add_constant(Constant::String(var_name.clone()));
810                        let prop_idx = self.chunk.add_constant(Constant::String(property.clone()));
811                        if let Some(op) = op {
812                            self.compile_node(target)?;
813                            self.compile_node(value)?;
814                            self.emit_compound_op(op)?;
815                        } else {
816                            self.compile_node(value)?;
817                        }
818                        // SetProperty reads var_idx from env, sets prop, writes back.
819                        // The variable name index is encoded as a second u16.
820                        self.chunk.emit_u16(Op::SetProperty, prop_idx, self.line);
821                        let hi = (var_idx >> 8) as u8;
822                        let lo = var_idx as u8;
823                        self.chunk.code.push(hi);
824                        self.chunk.code.push(lo);
825                        self.chunk.lines.push(self.line);
826                        self.chunk.columns.push(self.column);
827                        self.chunk.lines.push(self.line);
828                        self.chunk.columns.push(self.column);
829                    }
830                } else if let Node::SubscriptAccess { object, index } = &target.node {
831                    if let Some(var_name) = self.root_var_name(object) {
832                        let var_idx = self.chunk.add_constant(Constant::String(var_name.clone()));
833                        if let Some(op) = op {
834                            self.compile_node(target)?;
835                            self.compile_node(value)?;
836                            self.emit_compound_op(op)?;
837                        } else {
838                            self.compile_node(value)?;
839                        }
840                        self.compile_node(index)?;
841                        self.chunk.emit_u16(Op::SetSubscript, var_idx, self.line);
842                    }
843                }
844            }
845
846            Node::BinaryOp { op, left, right } => {
847                match op.as_str() {
848                    "&&" => {
849                        self.compile_node(left)?;
850                        let jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
851                        self.chunk.emit(Op::Pop, self.line);
852                        self.compile_node(right)?;
853                        self.chunk.patch_jump(jump);
854                        // Normalize to bool.
855                        self.chunk.emit(Op::Not, self.line);
856                        self.chunk.emit(Op::Not, self.line);
857                        return Ok(());
858                    }
859                    "||" => {
860                        self.compile_node(left)?;
861                        let jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
862                        self.chunk.emit(Op::Pop, self.line);
863                        self.compile_node(right)?;
864                        self.chunk.patch_jump(jump);
865                        self.chunk.emit(Op::Not, self.line);
866                        self.chunk.emit(Op::Not, self.line);
867                        return Ok(());
868                    }
869                    "??" => {
870                        self.compile_node(left)?;
871                        self.chunk.emit(Op::Dup, self.line);
872                        self.chunk.emit(Op::Nil, self.line);
873                        self.chunk.emit(Op::NotEqual, self.line);
874                        let jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
875                        self.chunk.emit(Op::Pop, self.line);
876                        self.chunk.emit(Op::Pop, self.line);
877                        self.compile_node(right)?;
878                        let end = self.chunk.emit_jump(Op::Jump, self.line);
879                        self.chunk.patch_jump(jump);
880                        self.chunk.emit(Op::Pop, self.line);
881                        self.chunk.patch_jump(end);
882                        return Ok(());
883                    }
884                    "|>" => {
885                        self.compile_node(left)?;
886                        // `value |> func(_, arg)` desugars to `value |> { __pipe -> func(__pipe, arg) }`.
887                        if contains_pipe_placeholder(right) {
888                            let replaced = replace_pipe_placeholder(right);
889                            let closure_node = SNode::dummy(Node::Closure {
890                                params: vec![TypedParam {
891                                    name: "__pipe".into(),
892                                    type_expr: None,
893                                    default_value: None,
894                                    rest: false,
895                                }],
896                                body: vec![replaced],
897                                fn_syntax: false,
898                            });
899                            self.compile_node(&closure_node)?;
900                        } else {
901                            self.compile_node(right)?;
902                        }
903                        self.chunk.emit(Op::Pipe, self.line);
904                        return Ok(());
905                    }
906                    _ => {}
907                }
908
909                self.compile_node(left)?;
910                self.compile_node(right)?;
911                match op.as_str() {
912                    "+" => self.chunk.emit(Op::Add, self.line),
913                    "-" => self.chunk.emit(Op::Sub, self.line),
914                    "*" => self.chunk.emit(Op::Mul, self.line),
915                    "/" => self.chunk.emit(Op::Div, self.line),
916                    "%" => self.chunk.emit(Op::Mod, self.line),
917                    "**" => self.chunk.emit(Op::Pow, self.line),
918                    "==" => self.chunk.emit(Op::Equal, self.line),
919                    "!=" => self.chunk.emit(Op::NotEqual, self.line),
920                    "<" => self.chunk.emit(Op::Less, self.line),
921                    ">" => self.chunk.emit(Op::Greater, self.line),
922                    "<=" => self.chunk.emit(Op::LessEqual, self.line),
923                    ">=" => self.chunk.emit(Op::GreaterEqual, self.line),
924                    "in" => self.chunk.emit(Op::Contains, self.line),
925                    "not_in" => {
926                        self.chunk.emit(Op::Contains, self.line);
927                        self.chunk.emit(Op::Not, self.line);
928                    }
929                    _ => {
930                        return Err(CompileError {
931                            message: format!("Unknown operator: {op}"),
932                            line: self.line,
933                        })
934                    }
935                }
936            }
937
938            Node::UnaryOp { op, operand } => {
939                self.compile_node(operand)?;
940                match op.as_str() {
941                    "-" => self.chunk.emit(Op::Negate, self.line),
942                    "!" => self.chunk.emit(Op::Not, self.line),
943                    _ => {}
944                }
945            }
946
947            Node::Ternary {
948                condition,
949                true_expr,
950                false_expr,
951            } => {
952                self.compile_node(condition)?;
953                let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
954                self.chunk.emit(Op::Pop, self.line);
955                self.compile_node(true_expr)?;
956                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
957                self.chunk.patch_jump(else_jump);
958                self.chunk.emit(Op::Pop, self.line);
959                self.compile_node(false_expr)?;
960                self.chunk.patch_jump(end_jump);
961            }
962
963            Node::FunctionCall { name, args } => {
964                // Compile-time lowering: `schema_of(TypeAlias)` emits the
965                // alias's JSON-Schema dict as a constant. Falls through to
966                // the runtime `schema_of(...)` builtin when the argument is
967                // not a known type alias (e.g. a string name computed at
968                // runtime, or a dict pass-through).
969                if name == "schema_of" && args.len() == 1 {
970                    if let Node::Identifier(alias) = &args[0].node {
971                        if let Some(schema) = self.schema_value_for_alias(alias) {
972                            self.emit_vm_value_literal(&schema);
973                            return Ok(());
974                        }
975                    }
976                }
977                // `schema_is(x, T)` / `schema_expect(x, T[, defaults])` /
978                // `schema_parse(x, T)` / `schema_check(x, T)` /
979                // `is_type(x, T)`: when the schema argument is a type-alias
980                // identifier, inline the alias's JSON-Schema dict as a
981                // constant. This is the counterpart to the parser-side
982                // narrowing in `schema_type_expr_from_node`.
983                if Self::is_schema_guard(name) && args.len() >= 2 {
984                    if let Node::Identifier(alias) = &args[1].node {
985                        if let Some(schema) = self.schema_value_for_alias(alias) {
986                            let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
987                            self.chunk.emit_u16(Op::Constant, name_idx, self.line);
988                            self.compile_node(&args[0])?;
989                            self.emit_vm_value_literal(&schema);
990                            for arg in &args[2..] {
991                                self.compile_node(arg)?;
992                            }
993                            self.chunk.emit_u8(Op::Call, args.len() as u8, self.line);
994                            return Ok(());
995                        }
996                    }
997                }
998
999                let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
1000                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1001                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
1002
1003                if has_spread {
1004                    // Flush-and-concat pattern: build args into one list
1005                    // (same as ListLiteral with spreads).
1006                    self.chunk.emit_u16(Op::BuildList, 0, self.line);
1007                    let mut pending = 0u16;
1008                    for arg in args {
1009                        if let Node::Spread(inner) = &arg.node {
1010                            if pending > 0 {
1011                                self.chunk.emit_u16(Op::BuildList, pending, self.line);
1012                                self.chunk.emit(Op::Add, self.line);
1013                                pending = 0;
1014                            }
1015                            self.compile_node(inner)?;
1016                            self.chunk.emit(Op::Dup, self.line);
1017                            let assert_idx = self
1018                                .chunk
1019                                .add_constant(Constant::String("__assert_list".into()));
1020                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
1021                            self.chunk.emit(Op::Swap, self.line);
1022                            self.chunk.emit_u8(Op::Call, 1, self.line);
1023                            self.chunk.emit(Op::Pop, self.line);
1024                            self.chunk.emit(Op::Add, self.line);
1025                        } else {
1026                            self.compile_node(arg)?;
1027                            pending += 1;
1028                        }
1029                    }
1030                    if pending > 0 {
1031                        self.chunk.emit_u16(Op::BuildList, pending, self.line);
1032                        self.chunk.emit(Op::Add, self.line);
1033                    }
1034                    self.chunk.emit(Op::CallSpread, self.line);
1035                } else {
1036                    for arg in args {
1037                        self.compile_node(arg)?;
1038                    }
1039                    self.chunk.emit_u8(Op::Call, args.len() as u8, self.line);
1040                }
1041            }
1042
1043            Node::MethodCall {
1044                object,
1045                method,
1046                args,
1047            } => {
1048                // EnumName.Variant(args) desugars to BuildEnum.
1049                if let Node::Identifier(name) = &object.node {
1050                    if self.enum_names.contains(name) {
1051                        for arg in args {
1052                            self.compile_node(arg)?;
1053                        }
1054                        let enum_idx = self.chunk.add_constant(Constant::String(name.clone()));
1055                        let var_idx = self.chunk.add_constant(Constant::String(method.clone()));
1056                        self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
1057                        let hi = (var_idx >> 8) as u8;
1058                        let lo = var_idx as u8;
1059                        self.chunk.code.push(hi);
1060                        self.chunk.code.push(lo);
1061                        self.chunk.lines.push(self.line);
1062                        self.chunk.columns.push(self.column);
1063                        self.chunk.lines.push(self.line);
1064                        self.chunk.columns.push(self.column);
1065                        let fc = args.len() as u16;
1066                        let fhi = (fc >> 8) as u8;
1067                        let flo = fc as u8;
1068                        self.chunk.code.push(fhi);
1069                        self.chunk.code.push(flo);
1070                        self.chunk.lines.push(self.line);
1071                        self.chunk.columns.push(self.column);
1072                        self.chunk.lines.push(self.line);
1073                        self.chunk.columns.push(self.column);
1074                        return Ok(());
1075                    }
1076                }
1077                let has_spread = args.iter().any(|a| matches!(&a.node, Node::Spread(_)));
1078                self.compile_node(object)?;
1079                let name_idx = self.chunk.add_constant(Constant::String(method.clone()));
1080                if has_spread {
1081                    self.chunk.emit_u16(Op::BuildList, 0, self.line);
1082                    let mut pending = 0u16;
1083                    for arg in args {
1084                        if let Node::Spread(inner) = &arg.node {
1085                            if pending > 0 {
1086                                self.chunk.emit_u16(Op::BuildList, pending, self.line);
1087                                self.chunk.emit(Op::Add, self.line);
1088                                pending = 0;
1089                            }
1090                            self.compile_node(inner)?;
1091                            self.chunk.emit(Op::Dup, self.line);
1092                            let assert_idx = self
1093                                .chunk
1094                                .add_constant(Constant::String("__assert_list".into()));
1095                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
1096                            self.chunk.emit(Op::Swap, self.line);
1097                            self.chunk.emit_u8(Op::Call, 1, self.line);
1098                            self.chunk.emit(Op::Pop, self.line);
1099                            self.chunk.emit(Op::Add, self.line);
1100                        } else {
1101                            self.compile_node(arg)?;
1102                            pending += 1;
1103                        }
1104                    }
1105                    if pending > 0 {
1106                        self.chunk.emit_u16(Op::BuildList, pending, self.line);
1107                        self.chunk.emit(Op::Add, self.line);
1108                    }
1109                    self.chunk
1110                        .emit_u16(Op::MethodCallSpread, name_idx, self.line);
1111                } else {
1112                    for arg in args {
1113                        self.compile_node(arg)?;
1114                    }
1115                    self.chunk
1116                        .emit_method_call(name_idx, args.len() as u8, self.line);
1117                }
1118            }
1119
1120            Node::OptionalMethodCall {
1121                object,
1122                method,
1123                args,
1124            } => {
1125                self.compile_node(object)?;
1126                for arg in args {
1127                    self.compile_node(arg)?;
1128                }
1129                let name_idx = self.chunk.add_constant(Constant::String(method.clone()));
1130                self.chunk
1131                    .emit_method_call_opt(name_idx, args.len() as u8, self.line);
1132            }
1133
1134            Node::PropertyAccess { object, property } => {
1135                // Bare `EnumName.Variant` desugars to a zero-field BuildEnum.
1136                if let Node::Identifier(name) = &object.node {
1137                    if self.enum_names.contains(name) {
1138                        let enum_idx = self.chunk.add_constant(Constant::String(name.clone()));
1139                        let var_idx = self.chunk.add_constant(Constant::String(property.clone()));
1140                        self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
1141                        let hi = (var_idx >> 8) as u8;
1142                        let lo = var_idx as u8;
1143                        self.chunk.code.push(hi);
1144                        self.chunk.code.push(lo);
1145                        self.chunk.lines.push(self.line);
1146                        self.chunk.columns.push(self.column);
1147                        self.chunk.lines.push(self.line);
1148                        self.chunk.columns.push(self.column);
1149                        self.chunk.code.push(0);
1150                        self.chunk.code.push(0);
1151                        self.chunk.lines.push(self.line);
1152                        self.chunk.columns.push(self.column);
1153                        self.chunk.lines.push(self.line);
1154                        self.chunk.columns.push(self.column);
1155                        return Ok(());
1156                    }
1157                }
1158                self.compile_node(object)?;
1159                let idx = self.chunk.add_constant(Constant::String(property.clone()));
1160                self.chunk.emit_u16(Op::GetProperty, idx, self.line);
1161            }
1162
1163            Node::OptionalPropertyAccess { object, property } => {
1164                self.compile_node(object)?;
1165                let idx = self.chunk.add_constant(Constant::String(property.clone()));
1166                self.chunk.emit_u16(Op::GetPropertyOpt, idx, self.line);
1167            }
1168
1169            Node::SubscriptAccess { object, index } => {
1170                self.compile_node(object)?;
1171                self.compile_node(index)?;
1172                self.chunk.emit(Op::Subscript, self.line);
1173            }
1174
1175            Node::SliceAccess { object, start, end } => {
1176                self.compile_node(object)?;
1177                if let Some(s) = start {
1178                    self.compile_node(s)?;
1179                } else {
1180                    self.chunk.emit(Op::Nil, self.line);
1181                }
1182                if let Some(e) = end {
1183                    self.compile_node(e)?;
1184                } else {
1185                    self.chunk.emit(Op::Nil, self.line);
1186                }
1187                self.chunk.emit(Op::Slice, self.line);
1188            }
1189
1190            Node::IfElse {
1191                condition,
1192                then_body,
1193                else_body,
1194            } => {
1195                self.compile_node(condition)?;
1196                let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1197                self.chunk.emit(Op::Pop, self.line);
1198                self.compile_scoped_block(then_body)?;
1199                if let Some(else_body) = else_body {
1200                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1201                    self.chunk.patch_jump(else_jump);
1202                    self.chunk.emit(Op::Pop, self.line);
1203                    self.compile_scoped_block(else_body)?;
1204                    self.chunk.patch_jump(end_jump);
1205                } else {
1206                    self.chunk.patch_jump(else_jump);
1207                    self.chunk.emit(Op::Pop, self.line);
1208                    self.chunk.emit(Op::Nil, self.line);
1209                }
1210            }
1211
1212            Node::WhileLoop { condition, body } => {
1213                let loop_start = self.chunk.current_offset();
1214                self.loop_stack.push(LoopContext {
1215                    start_offset: loop_start,
1216                    break_patches: Vec::new(),
1217                    has_iterator: false,
1218                    handler_depth: self.handler_depth,
1219                    finally_depth: self.finally_bodies.len(),
1220                    scope_depth: self.scope_depth,
1221                });
1222                self.compile_node(condition)?;
1223                let exit_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1224                self.chunk.emit(Op::Pop, self.line);
1225                self.compile_scoped_statements(body)?;
1226                // Jump back to condition
1227                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
1228                self.chunk.patch_jump(exit_jump);
1229                self.chunk.emit(Op::Pop, self.line);
1230                let ctx = self.loop_stack.pop().unwrap();
1231                for patch_pos in ctx.break_patches {
1232                    self.chunk.patch_jump(patch_pos);
1233                }
1234                self.chunk.emit(Op::Nil, self.line);
1235            }
1236
1237            Node::ForIn {
1238                pattern,
1239                iterable,
1240                body,
1241            } => {
1242                self.compile_node(iterable)?;
1243                self.chunk.emit(Op::IterInit, self.line);
1244                let loop_start = self.chunk.current_offset();
1245                self.loop_stack.push(LoopContext {
1246                    start_offset: loop_start,
1247                    break_patches: Vec::new(),
1248                    has_iterator: true,
1249                    handler_depth: self.handler_depth,
1250                    finally_depth: self.finally_bodies.len(),
1251                    scope_depth: self.scope_depth,
1252                });
1253                // IterNext jumps to end if exhausted, else pushes the next item.
1254                let exit_jump_pos = self.chunk.emit_jump(Op::IterNext, self.line);
1255                self.begin_scope();
1256                self.compile_destructuring(pattern, true)?;
1257                for sn in body {
1258                    self.compile_node(sn)?;
1259                    if Self::produces_value(&sn.node) {
1260                        self.chunk.emit(Op::Pop, self.line);
1261                    }
1262                }
1263                self.end_scope();
1264                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
1265                self.chunk.patch_jump(exit_jump_pos);
1266                let ctx = self.loop_stack.pop().unwrap();
1267                for patch_pos in ctx.break_patches {
1268                    self.chunk.patch_jump(patch_pos);
1269                }
1270                self.chunk.emit(Op::Nil, self.line);
1271            }
1272
1273            Node::ReturnStmt { value } => {
1274                if self.has_pending_finally() {
1275                    // Inside try-finally: save value to a temp, run pending
1276                    // finallys, then restore and return.
1277                    if let Some(val) = value {
1278                        self.compile_node(val)?;
1279                    } else {
1280                        self.chunk.emit(Op::Nil, self.line);
1281                    }
1282                    self.temp_counter += 1;
1283                    let temp_name = format!("__return_val_{}__", self.temp_counter);
1284                    let save_idx = self.chunk.add_constant(Constant::String(temp_name.clone()));
1285                    self.chunk.emit_u16(Op::DefVar, save_idx, self.line);
1286                    // Innermost finally first; skip catch barriers since
1287                    // return transfers past local handlers.
1288                    for fb in self.all_pending_finallys() {
1289                        self.compile_finally_inline(&fb)?;
1290                    }
1291                    let restore_idx = self.chunk.add_constant(Constant::String(temp_name));
1292                    self.chunk.emit_u16(Op::GetVar, restore_idx, self.line);
1293                    self.chunk.emit(Op::Return, self.line);
1294                } else {
1295                    // No pending finally — use tail-call optimization when possible.
1296                    if let Some(val) = value {
1297                        if let Node::FunctionCall { name, args } = &val.node {
1298                            let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1299                            self.chunk.emit_u16(Op::Constant, name_idx, self.line);
1300                            for arg in args {
1301                                self.compile_node(arg)?;
1302                            }
1303                            self.chunk
1304                                .emit_u8(Op::TailCall, args.len() as u8, self.line);
1305                        } else if let Node::BinaryOp { op, left, right } = &val.node {
1306                            if op == "|>" {
1307                                self.compile_node(left)?;
1308                                self.compile_node(right)?;
1309                                self.chunk.emit(Op::Swap, self.line);
1310                                self.chunk.emit_u8(Op::TailCall, 1, self.line);
1311                            } else {
1312                                self.compile_node(val)?;
1313                            }
1314                        } else {
1315                            self.compile_node(val)?;
1316                        }
1317                    } else {
1318                        self.chunk.emit(Op::Nil, self.line);
1319                    }
1320                    self.chunk.emit(Op::Return, self.line);
1321                }
1322            }
1323
1324            Node::BreakStmt => {
1325                if self.loop_stack.is_empty() {
1326                    return Err(CompileError {
1327                        message: "break outside of loop".to_string(),
1328                        line: self.line,
1329                    });
1330                }
1331                // Copy values out to avoid borrow conflict.
1332                let ctx = self.loop_stack.last().unwrap();
1333                let finally_depth = ctx.finally_depth;
1334                let handler_depth = ctx.handler_depth;
1335                let has_iterator = ctx.has_iterator;
1336                let scope_depth = ctx.scope_depth;
1337                for _ in handler_depth..self.handler_depth {
1338                    self.chunk.emit(Op::PopHandler, self.line);
1339                }
1340                for fb in self.pending_finallys_down_to(finally_depth) {
1341                    self.compile_finally_inline(&fb)?;
1342                }
1343                self.unwind_scopes_to(scope_depth);
1344                if has_iterator {
1345                    self.chunk.emit(Op::PopIterator, self.line);
1346                }
1347                let patch = self.chunk.emit_jump(Op::Jump, self.line);
1348                self.loop_stack
1349                    .last_mut()
1350                    .unwrap()
1351                    .break_patches
1352                    .push(patch);
1353            }
1354
1355            Node::ContinueStmt => {
1356                if self.loop_stack.is_empty() {
1357                    return Err(CompileError {
1358                        message: "continue outside of loop".to_string(),
1359                        line: self.line,
1360                    });
1361                }
1362                let ctx = self.loop_stack.last().unwrap();
1363                let finally_depth = ctx.finally_depth;
1364                let handler_depth = ctx.handler_depth;
1365                let loop_start = ctx.start_offset;
1366                let scope_depth = ctx.scope_depth;
1367                for _ in handler_depth..self.handler_depth {
1368                    self.chunk.emit(Op::PopHandler, self.line);
1369                }
1370                for fb in self.pending_finallys_down_to(finally_depth) {
1371                    self.compile_finally_inline(&fb)?;
1372                }
1373                self.unwind_scopes_to(scope_depth);
1374                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
1375            }
1376
1377            Node::ListLiteral(elements) => {
1378                let has_spread = elements.iter().any(|e| matches!(&e.node, Node::Spread(_)));
1379                if !has_spread {
1380                    for el in elements {
1381                        self.compile_node(el)?;
1382                    }
1383                    self.chunk
1384                        .emit_u16(Op::BuildList, elements.len() as u16, self.line);
1385                } else {
1386                    // Flush-and-concat: accumulate non-spread elements and concat with spread lists.
1387                    self.chunk.emit_u16(Op::BuildList, 0, self.line);
1388                    let mut pending = 0u16;
1389                    for el in elements {
1390                        if let Node::Spread(inner) = &el.node {
1391                            if pending > 0 {
1392                                self.chunk.emit_u16(Op::BuildList, pending, self.line);
1393                                self.chunk.emit(Op::Add, self.line);
1394                                pending = 0;
1395                            }
1396                            self.compile_node(inner)?;
1397                            self.chunk.emit(Op::Dup, self.line);
1398                            let assert_idx = self
1399                                .chunk
1400                                .add_constant(Constant::String("__assert_list".into()));
1401                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
1402                            self.chunk.emit(Op::Swap, self.line);
1403                            self.chunk.emit_u8(Op::Call, 1, self.line);
1404                            self.chunk.emit(Op::Pop, self.line);
1405                            self.chunk.emit(Op::Add, self.line);
1406                        } else {
1407                            self.compile_node(el)?;
1408                            pending += 1;
1409                        }
1410                    }
1411                    if pending > 0 {
1412                        self.chunk.emit_u16(Op::BuildList, pending, self.line);
1413                        self.chunk.emit(Op::Add, self.line);
1414                    }
1415                }
1416            }
1417
1418            Node::DictLiteral(entries) => {
1419                let has_spread = entries
1420                    .iter()
1421                    .any(|e| matches!(&e.value.node, Node::Spread(_)));
1422                if !has_spread {
1423                    for entry in entries {
1424                        self.compile_node(&entry.key)?;
1425                        // Sugar: `output_schema: TypeAlias` inside an
1426                        // `llm_call(..., { ... })` options dict lowers to
1427                        // the alias's JSON-Schema dict constant. This lets
1428                        // users reuse one `type T = { ... }` declaration
1429                        // for both type-checking and structured-output
1430                        // validation. Falls through to the normal expression
1431                        // path when the name does not resolve.
1432                        if Self::entry_key_is(&entry.key, "output_schema") {
1433                            if let Node::Identifier(alias) = &entry.value.node {
1434                                if let Some(schema) = self.schema_value_for_alias(alias) {
1435                                    self.emit_vm_value_literal(&schema);
1436                                    continue;
1437                                }
1438                            }
1439                        }
1440                        self.compile_node(&entry.value)?;
1441                    }
1442                    self.chunk
1443                        .emit_u16(Op::BuildDict, entries.len() as u16, self.line);
1444                } else {
1445                    // Flush-and-merge via Add on empty dict.
1446                    self.chunk.emit_u16(Op::BuildDict, 0, self.line);
1447                    let mut pending = 0u16;
1448                    for entry in entries {
1449                        if let Node::Spread(inner) = &entry.value.node {
1450                            if pending > 0 {
1451                                self.chunk.emit_u16(Op::BuildDict, pending, self.line);
1452                                self.chunk.emit(Op::Add, self.line);
1453                                pending = 0;
1454                            }
1455                            self.compile_node(inner)?;
1456                            self.chunk.emit(Op::Dup, self.line);
1457                            let assert_idx = self
1458                                .chunk
1459                                .add_constant(Constant::String("__assert_dict".into()));
1460                            self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
1461                            self.chunk.emit(Op::Swap, self.line);
1462                            self.chunk.emit_u8(Op::Call, 1, self.line);
1463                            self.chunk.emit(Op::Pop, self.line);
1464                            self.chunk.emit(Op::Add, self.line);
1465                        } else {
1466                            self.compile_node(&entry.key)?;
1467                            self.compile_node(&entry.value)?;
1468                            pending += 1;
1469                        }
1470                    }
1471                    if pending > 0 {
1472                        self.chunk.emit_u16(Op::BuildDict, pending, self.line);
1473                        self.chunk.emit(Op::Add, self.line);
1474                    }
1475                }
1476            }
1477
1478            Node::InterpolatedString(segments) => {
1479                let mut part_count = 0u16;
1480                for seg in segments {
1481                    match seg {
1482                        StringSegment::Literal(s) => {
1483                            let idx = self.chunk.add_constant(Constant::String(s.clone()));
1484                            self.chunk.emit_u16(Op::Constant, idx, self.line);
1485                            part_count += 1;
1486                        }
1487                        StringSegment::Expression(expr_str, expr_line, expr_col) => {
1488                            let mut lexer =
1489                                harn_lexer::Lexer::with_position(expr_str, *expr_line, *expr_col);
1490                            if let Ok(tokens) = lexer.tokenize() {
1491                                let mut parser = harn_parser::Parser::new(tokens);
1492                                if let Ok(snode) = parser.parse_single_expression() {
1493                                    self.compile_node(&snode)?;
1494                                    let to_str = self
1495                                        .chunk
1496                                        .add_constant(Constant::String("to_string".into()));
1497                                    self.chunk.emit_u16(Op::Constant, to_str, self.line);
1498                                    self.chunk.emit(Op::Swap, self.line);
1499                                    self.chunk.emit_u8(Op::Call, 1, self.line);
1500                                    part_count += 1;
1501                                } else {
1502                                    // Fallback: treat as literal.
1503                                    let idx =
1504                                        self.chunk.add_constant(Constant::String(expr_str.clone()));
1505                                    self.chunk.emit_u16(Op::Constant, idx, self.line);
1506                                    part_count += 1;
1507                                }
1508                            }
1509                        }
1510                    }
1511                }
1512                if part_count > 1 {
1513                    self.chunk.emit_u16(Op::Concat, part_count, self.line);
1514                }
1515            }
1516
1517            Node::FnDecl {
1518                name, params, body, ..
1519            } => {
1520                let mut fn_compiler = Compiler::for_nested_body();
1521                fn_compiler.enum_names = self.enum_names.clone();
1522                fn_compiler.emit_default_preamble(params)?;
1523                fn_compiler.emit_type_checks(params);
1524                let is_gen = body_contains_yield(body);
1525                fn_compiler.compile_block(body)?;
1526                // Run pending defers before implicit return
1527                for fb in fn_compiler.all_pending_finallys() {
1528                    fn_compiler.compile_finally_inline(&fb)?;
1529                }
1530                fn_compiler.chunk.emit(Op::Nil, self.line);
1531                fn_compiler.chunk.emit(Op::Return, self.line);
1532
1533                let func = CompiledFunction {
1534                    name: name.clone(),
1535                    params: TypedParam::names(params),
1536                    default_start: TypedParam::default_start(params),
1537                    chunk: fn_compiler.chunk,
1538                    is_generator: is_gen,
1539                    has_rest_param: params.last().is_some_and(|p| p.rest),
1540                };
1541                let fn_idx = self.chunk.functions.len();
1542                self.chunk.functions.push(func);
1543
1544                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1545                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1546                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1547            }
1548
1549            Node::ToolDecl {
1550                name,
1551                description,
1552                params,
1553                return_type,
1554                body,
1555                ..
1556            } => {
1557                // Compile the body as a closure, then call `tool_define(registry, name, description, config)`.
1558                let mut fn_compiler = Compiler::for_nested_body();
1559                fn_compiler.enum_names = self.enum_names.clone();
1560                fn_compiler.emit_default_preamble(params)?;
1561                fn_compiler.emit_type_checks(params);
1562                fn_compiler.compile_block(body)?;
1563                // Run pending defers before implicit return
1564                for fb in fn_compiler.all_pending_finallys() {
1565                    fn_compiler.compile_finally_inline(&fb)?;
1566                }
1567                fn_compiler.chunk.emit(Op::Return, self.line);
1568
1569                let func = CompiledFunction {
1570                    name: name.clone(),
1571                    params: TypedParam::names(params),
1572                    default_start: TypedParam::default_start(params),
1573                    chunk: fn_compiler.chunk,
1574                    is_generator: false,
1575                    has_rest_param: params.last().is_some_and(|p| p.rest),
1576                };
1577                let fn_idx = self.chunk.functions.len();
1578                self.chunk.functions.push(func);
1579
1580                let define_name = self
1581                    .chunk
1582                    .add_constant(Constant::String("tool_define".into()));
1583                self.chunk.emit_u16(Op::Constant, define_name, self.line);
1584
1585                let reg_name = self
1586                    .chunk
1587                    .add_constant(Constant::String("tool_registry".into()));
1588                self.chunk.emit_u16(Op::Constant, reg_name, self.line);
1589                self.chunk.emit_u8(Op::Call, 0, self.line);
1590
1591                let tool_name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1592                self.chunk.emit_u16(Op::Constant, tool_name_idx, self.line);
1593
1594                let desc = description.as_deref().unwrap_or("");
1595                let desc_idx = self.chunk.add_constant(Constant::String(desc.to_string()));
1596                self.chunk.emit_u16(Op::Constant, desc_idx, self.line);
1597
1598                // Build parameters dict using the same schema lowering as
1599                // runtime param validation so tools expose nested shapes,
1600                // unions, item schemas, defaults, and dict value schemas.
1601                let mut param_count: u16 = 0;
1602                for p in params {
1603                    let pn_idx = self.chunk.add_constant(Constant::String(p.name.clone()));
1604                    self.chunk.emit_u16(Op::Constant, pn_idx, self.line);
1605
1606                    let base_schema = p
1607                        .type_expr
1608                        .as_ref()
1609                        .and_then(Self::type_expr_to_schema_value)
1610                        .unwrap_or_else(|| {
1611                            VmValue::Dict(Rc::new(BTreeMap::from([(
1612                                "type".to_string(),
1613                                VmValue::String(Rc::from("any")),
1614                            )])))
1615                        });
1616                    let public_schema =
1617                        schema::schema_to_json_schema_value(&base_schema).map_err(|error| {
1618                            CompileError {
1619                                message: format!(
1620                                    "failed to lower tool parameter schema for '{}': {}",
1621                                    p.name, error
1622                                ),
1623                                line: self.line,
1624                            }
1625                        })?;
1626                    let mut param_schema = match public_schema {
1627                        VmValue::Dict(map) => (*map).clone(),
1628                        _ => BTreeMap::new(),
1629                    };
1630
1631                    if p.default_value.is_some() {
1632                        param_schema.insert("required".to_string(), VmValue::Bool(false));
1633                    }
1634
1635                    self.emit_vm_value_literal(&VmValue::Dict(Rc::new(param_schema)));
1636
1637                    if let Some(default_value) = p.default_value.as_ref() {
1638                        let default_key =
1639                            self.chunk.add_constant(Constant::String("default".into()));
1640                        self.chunk.emit_u16(Op::Constant, default_key, self.line);
1641                        self.compile_node(default_value)?;
1642                        self.chunk.emit_u16(Op::BuildDict, 1, self.line);
1643                        self.chunk.emit(Op::Add, self.line);
1644                    }
1645
1646                    param_count += 1;
1647                }
1648                self.chunk.emit_u16(Op::BuildDict, param_count, self.line);
1649
1650                let params_key = self
1651                    .chunk
1652                    .add_constant(Constant::String("parameters".into()));
1653                self.chunk.emit_u16(Op::Constant, params_key, self.line);
1654                self.chunk.emit(Op::Swap, self.line);
1655
1656                let handler_key = self.chunk.add_constant(Constant::String("handler".into()));
1657                self.chunk.emit_u16(Op::Constant, handler_key, self.line);
1658                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1659
1660                let mut config_entries = 2u16;
1661                if let Some(return_type) = return_type
1662                    .as_ref()
1663                    .and_then(Self::type_expr_to_schema_value)
1664                {
1665                    let return_type =
1666                        schema::schema_to_json_schema_value(&return_type).map_err(|error| {
1667                            CompileError {
1668                                message: format!(
1669                                    "failed to lower tool return schema for '{}': {}",
1670                                    name, error
1671                                ),
1672                                line: self.line,
1673                            }
1674                        })?;
1675                    let returns_key = self.chunk.add_constant(Constant::String("returns".into()));
1676                    self.chunk.emit_u16(Op::Constant, returns_key, self.line);
1677                    self.emit_vm_value_literal(&return_type);
1678                    config_entries += 1;
1679                }
1680
1681                self.chunk
1682                    .emit_u16(Op::BuildDict, config_entries, self.line);
1683
1684                self.chunk.emit_u8(Op::Call, 4, self.line);
1685
1686                let bind_idx = self.chunk.add_constant(Constant::String(name.clone()));
1687                self.chunk.emit_u16(Op::DefLet, bind_idx, self.line);
1688            }
1689
1690            Node::Closure { params, body, .. } => {
1691                let mut fn_compiler = Compiler::for_nested_body();
1692                fn_compiler.enum_names = self.enum_names.clone();
1693                fn_compiler.emit_default_preamble(params)?;
1694                fn_compiler.emit_type_checks(params);
1695                let is_gen = body_contains_yield(body);
1696                fn_compiler.compile_block(body)?;
1697                // Run pending defers before implicit return
1698                for fb in fn_compiler.all_pending_finallys() {
1699                    fn_compiler.compile_finally_inline(&fb)?;
1700                }
1701                fn_compiler.chunk.emit(Op::Return, self.line);
1702
1703                let func = CompiledFunction {
1704                    name: "<closure>".to_string(),
1705                    params: TypedParam::names(params),
1706                    default_start: TypedParam::default_start(params),
1707                    chunk: fn_compiler.chunk,
1708                    is_generator: is_gen,
1709                    has_rest_param: false,
1710                };
1711                let fn_idx = self.chunk.functions.len();
1712                self.chunk.functions.push(func);
1713
1714                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
1715            }
1716
1717            Node::ThrowStmt { value } => {
1718                // Only run finallys the unwind will actually cross — i.e.,
1719                // those between this throw and the innermost `CatchBarrier`.
1720                // Finallys beyond the nearest local `catch` aren't on the
1721                // throw's escape path (the catch halts unwinding there), so
1722                // pre-running them wrongly fires outer side effects.
1723                let pending = self.pending_finallys_until_barrier();
1724                if !pending.is_empty() {
1725                    self.compile_node(value)?;
1726                    self.temp_counter += 1;
1727                    let temp_name = format!("__throw_val_{}__", self.temp_counter);
1728                    let save_idx = self.chunk.add_constant(Constant::String(temp_name.clone()));
1729                    self.chunk.emit_u16(Op::DefVar, save_idx, self.line);
1730                    for fb in &pending {
1731                        self.compile_finally_inline(fb)?;
1732                    }
1733                    let restore_idx = self.chunk.add_constant(Constant::String(temp_name));
1734                    self.chunk.emit_u16(Op::GetVar, restore_idx, self.line);
1735                    self.chunk.emit(Op::Throw, self.line);
1736                } else {
1737                    self.compile_node(value)?;
1738                    self.chunk.emit(Op::Throw, self.line);
1739                }
1740            }
1741
1742            Node::MatchExpr { value, arms } => {
1743                self.compile_node(value)?;
1744                let mut end_jumps = Vec::new();
1745                for arm in arms {
1746                    match &arm.pattern.node {
1747                        // Wildcard `_` — always matches (unless guarded)
1748                        Node::Identifier(name) if name == "_" => {
1749                            if let Some(ref guard) = arm.guard {
1750                                self.compile_node(guard)?;
1751                                let guard_skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1752                                self.chunk.emit(Op::Pop, self.line);
1753                                self.begin_scope();
1754                                self.chunk.emit(Op::Pop, self.line);
1755                                self.compile_match_body(&arm.body)?;
1756                                self.end_scope();
1757                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1758                                self.chunk.patch_jump(guard_skip);
1759                                self.chunk.emit(Op::Pop, self.line);
1760                            } else {
1761                                self.begin_scope();
1762                                self.chunk.emit(Op::Pop, self.line);
1763                                self.compile_match_body(&arm.body)?;
1764                                self.end_scope();
1765                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1766                            }
1767                        }
1768                        // Enum destructuring: EnumConstruct pattern
1769                        Node::EnumConstruct {
1770                            enum_name,
1771                            variant,
1772                            args: pat_args,
1773                        } => {
1774                            self.chunk.emit(Op::Dup, self.line);
1775                            let en_idx =
1776                                self.chunk.add_constant(Constant::String(enum_name.clone()));
1777                            let vn_idx = self.chunk.add_constant(Constant::String(variant.clone()));
1778                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1779                            let hi = (vn_idx >> 8) as u8;
1780                            let lo = vn_idx as u8;
1781                            self.chunk.code.push(hi);
1782                            self.chunk.code.push(lo);
1783                            self.chunk.lines.push(self.line);
1784                            self.chunk.columns.push(self.column);
1785                            self.chunk.lines.push(self.line);
1786                            self.chunk.columns.push(self.column);
1787                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1788                            self.chunk.emit(Op::Pop, self.line);
1789                            self.begin_scope();
1790
1791                            // Bind field variables from the enum's fields; the
1792                            // match value stays on the stack for extraction.
1793                            for (i, pat_arg) in pat_args.iter().enumerate() {
1794                                if let Node::Identifier(binding_name) = &pat_arg.node {
1795                                    self.chunk.emit(Op::Dup, self.line);
1796                                    let fields_idx = self
1797                                        .chunk
1798                                        .add_constant(Constant::String("fields".to_string()));
1799                                    self.chunk.emit_u16(Op::GetProperty, fields_idx, self.line);
1800                                    let idx_const =
1801                                        self.chunk.add_constant(Constant::Int(i as i64));
1802                                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1803                                    self.chunk.emit(Op::Subscript, self.line);
1804                                    let name_idx = self
1805                                        .chunk
1806                                        .add_constant(Constant::String(binding_name.clone()));
1807                                    self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1808                                }
1809                            }
1810
1811                            // Optional guard
1812                            if let Some(ref guard) = arm.guard {
1813                                self.compile_node(guard)?;
1814                                let guard_skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1815                                self.chunk.emit(Op::Pop, self.line);
1816                                self.chunk.emit(Op::Pop, self.line);
1817                                self.compile_match_body(&arm.body)?;
1818                                self.end_scope();
1819                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1820                                self.chunk.patch_jump(guard_skip);
1821                                self.chunk.emit(Op::Pop, self.line);
1822                                self.end_scope();
1823                            } else {
1824                                self.chunk.emit(Op::Pop, self.line);
1825                                self.compile_match_body(&arm.body)?;
1826                                self.end_scope();
1827                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1828                            }
1829                            self.chunk.patch_jump(skip);
1830                            self.chunk.emit(Op::Pop, self.line);
1831                        }
1832                        // Enum variant without args: PropertyAccess(EnumName, Variant)
1833                        Node::PropertyAccess { object, property } if matches!(&object.node, Node::Identifier(n) if self.enum_names.contains(n)) =>
1834                        {
1835                            let enum_name = if let Node::Identifier(n) = &object.node {
1836                                n.clone()
1837                            } else {
1838                                unreachable!()
1839                            };
1840                            self.chunk.emit(Op::Dup, self.line);
1841                            let en_idx = self.chunk.add_constant(Constant::String(enum_name));
1842                            let vn_idx =
1843                                self.chunk.add_constant(Constant::String(property.clone()));
1844                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1845                            let hi = (vn_idx >> 8) as u8;
1846                            let lo = vn_idx as u8;
1847                            self.chunk.code.push(hi);
1848                            self.chunk.code.push(lo);
1849                            self.chunk.lines.push(self.line);
1850                            self.chunk.columns.push(self.column);
1851                            self.chunk.lines.push(self.line);
1852                            self.chunk.columns.push(self.column);
1853                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1854                            self.chunk.emit(Op::Pop, self.line);
1855                            // Optional guard
1856                            if let Some(ref guard) = arm.guard {
1857                                self.compile_node(guard)?;
1858                                let guard_skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1859                                self.chunk.emit(Op::Pop, self.line);
1860                                self.begin_scope();
1861                                self.chunk.emit(Op::Pop, self.line);
1862                                self.compile_match_body(&arm.body)?;
1863                                self.end_scope();
1864                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1865                                self.chunk.patch_jump(guard_skip);
1866                                self.chunk.emit(Op::Pop, self.line);
1867                            } else {
1868                                self.begin_scope();
1869                                self.chunk.emit(Op::Pop, self.line);
1870                                self.compile_match_body(&arm.body)?;
1871                                self.end_scope();
1872                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1873                            }
1874                            self.chunk.patch_jump(skip);
1875                            self.chunk.emit(Op::Pop, self.line);
1876                        }
1877                        // Enum destructuring via MethodCall: EnumName.Variant(bindings...)
1878                        // Parser produces MethodCall for EnumName.Variant(x) patterns
1879                        Node::MethodCall {
1880                            object,
1881                            method,
1882                            args: pat_args,
1883                        } if matches!(&object.node, Node::Identifier(n) if self.enum_names.contains(n)) =>
1884                        {
1885                            let enum_name = if let Node::Identifier(n) = &object.node {
1886                                n.clone()
1887                            } else {
1888                                unreachable!()
1889                            };
1890                            self.chunk.emit(Op::Dup, self.line);
1891                            let en_idx = self.chunk.add_constant(Constant::String(enum_name));
1892                            let vn_idx = self.chunk.add_constant(Constant::String(method.clone()));
1893                            self.chunk.emit_u16(Op::MatchEnum, en_idx, self.line);
1894                            let hi = (vn_idx >> 8) as u8;
1895                            let lo = vn_idx as u8;
1896                            self.chunk.code.push(hi);
1897                            self.chunk.code.push(lo);
1898                            self.chunk.lines.push(self.line);
1899                            self.chunk.columns.push(self.column);
1900                            self.chunk.lines.push(self.line);
1901                            self.chunk.columns.push(self.column);
1902                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1903                            self.chunk.emit(Op::Pop, self.line);
1904                            self.begin_scope();
1905
1906                            for (i, pat_arg) in pat_args.iter().enumerate() {
1907                                if let Node::Identifier(binding_name) = &pat_arg.node {
1908                                    self.chunk.emit(Op::Dup, self.line);
1909                                    let fields_idx = self
1910                                        .chunk
1911                                        .add_constant(Constant::String("fields".to_string()));
1912                                    self.chunk.emit_u16(Op::GetProperty, fields_idx, self.line);
1913                                    let idx_const =
1914                                        self.chunk.add_constant(Constant::Int(i as i64));
1915                                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
1916                                    self.chunk.emit(Op::Subscript, self.line);
1917                                    let name_idx = self
1918                                        .chunk
1919                                        .add_constant(Constant::String(binding_name.clone()));
1920                                    self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1921                                }
1922                            }
1923
1924                            // Optional guard
1925                            if let Some(ref guard) = arm.guard {
1926                                self.compile_node(guard)?;
1927                                let guard_skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1928                                self.chunk.emit(Op::Pop, self.line);
1929                                self.chunk.emit(Op::Pop, self.line);
1930                                self.compile_match_body(&arm.body)?;
1931                                self.end_scope();
1932                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1933                                self.chunk.patch_jump(guard_skip);
1934                                self.chunk.emit(Op::Pop, self.line);
1935                                self.end_scope();
1936                            } else {
1937                                self.chunk.emit(Op::Pop, self.line);
1938                                self.compile_match_body(&arm.body)?;
1939                                self.end_scope();
1940                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1941                            }
1942                            self.chunk.patch_jump(skip);
1943                            self.chunk.emit(Op::Pop, self.line);
1944                        }
1945                        // Binding pattern: bare identifier always matches.
1946                        Node::Identifier(name) => {
1947                            self.begin_scope();
1948                            self.chunk.emit(Op::Dup, self.line);
1949                            let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
1950                            self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
1951                            // Optional guard
1952                            if let Some(ref guard) = arm.guard {
1953                                self.compile_node(guard)?;
1954                                let guard_skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1955                                self.chunk.emit(Op::Pop, self.line);
1956                                self.chunk.emit(Op::Pop, self.line);
1957                                self.compile_match_body(&arm.body)?;
1958                                self.end_scope();
1959                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1960                                self.chunk.patch_jump(guard_skip);
1961                                self.chunk.emit(Op::Pop, self.line);
1962                                self.end_scope();
1963                            } else {
1964                                self.chunk.emit(Op::Pop, self.line);
1965                                self.compile_match_body(&arm.body)?;
1966                                self.end_scope();
1967                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
1968                            }
1969                        }
1970                        // Dict pattern: {key: literal, key: binding, ...}
1971                        Node::DictLiteral(entries)
1972                            if entries
1973                                .iter()
1974                                .all(|e| matches!(&e.key.node, Node::StringLiteral(_))) =>
1975                        {
1976                            self.chunk.emit(Op::Dup, self.line);
1977                            let typeof_idx =
1978                                self.chunk.add_constant(Constant::String("type_of".into()));
1979                            self.chunk.emit_u16(Op::Constant, typeof_idx, self.line);
1980                            self.chunk.emit(Op::Swap, self.line);
1981                            self.chunk.emit_u8(Op::Call, 1, self.line);
1982                            let dict_str = self.chunk.add_constant(Constant::String("dict".into()));
1983                            self.chunk.emit_u16(Op::Constant, dict_str, self.line);
1984                            self.chunk.emit(Op::Equal, self.line);
1985                            let skip_type = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
1986                            self.chunk.emit(Op::Pop, self.line);
1987
1988                            let mut constraint_skips = Vec::new();
1989                            let mut bindings = Vec::new();
1990                            self.begin_scope();
1991                            for entry in entries {
1992                                if let Node::StringLiteral(key) = &entry.key.node {
1993                                    match &entry.value.node {
1994                                        Node::StringLiteral(_)
1995                                        | Node::IntLiteral(_)
1996                                        | Node::FloatLiteral(_)
1997                                        | Node::BoolLiteral(_)
1998                                        | Node::NilLiteral => {
1999                                            self.chunk.emit(Op::Dup, self.line);
2000                                            let key_idx = self
2001                                                .chunk
2002                                                .add_constant(Constant::String(key.clone()));
2003                                            self.chunk.emit_u16(Op::Constant, key_idx, self.line);
2004                                            self.chunk.emit(Op::Subscript, self.line);
2005                                            self.compile_node(&entry.value)?;
2006                                            self.chunk.emit(Op::Equal, self.line);
2007                                            let skip =
2008                                                self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2009                                            self.chunk.emit(Op::Pop, self.line);
2010                                            constraint_skips.push(skip);
2011                                        }
2012                                        Node::Identifier(binding) => {
2013                                            bindings.push((key.clone(), binding.clone()));
2014                                        }
2015                                        _ => {
2016                                            // Complex expression constraint: dict[key] == expr.
2017                                            self.chunk.emit(Op::Dup, self.line);
2018                                            let key_idx = self
2019                                                .chunk
2020                                                .add_constant(Constant::String(key.clone()));
2021                                            self.chunk.emit_u16(Op::Constant, key_idx, self.line);
2022                                            self.chunk.emit(Op::Subscript, self.line);
2023                                            self.compile_node(&entry.value)?;
2024                                            self.chunk.emit(Op::Equal, self.line);
2025                                            let skip =
2026                                                self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2027                                            self.chunk.emit(Op::Pop, self.line);
2028                                            constraint_skips.push(skip);
2029                                        }
2030                                    }
2031                                }
2032                            }
2033
2034                            for (key, binding) in &bindings {
2035                                self.chunk.emit(Op::Dup, self.line);
2036                                let key_idx =
2037                                    self.chunk.add_constant(Constant::String(key.clone()));
2038                                self.chunk.emit_u16(Op::Constant, key_idx, self.line);
2039                                self.chunk.emit(Op::Subscript, self.line);
2040                                let name_idx =
2041                                    self.chunk.add_constant(Constant::String(binding.clone()));
2042                                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
2043                            }
2044
2045                            // Optional guard
2046                            if let Some(ref guard) = arm.guard {
2047                                self.compile_node(guard)?;
2048                                let guard_skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2049                                self.chunk.emit(Op::Pop, self.line);
2050                                self.chunk.emit(Op::Pop, self.line);
2051                                self.compile_match_body(&arm.body)?;
2052                                self.end_scope();
2053                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
2054                                self.chunk.patch_jump(guard_skip);
2055                                // Guard failed: pop guard bool, fall through to scope cleanup below.
2056                                self.chunk.emit(Op::Pop, self.line);
2057                            } else {
2058                                self.chunk.emit(Op::Pop, self.line);
2059                                self.compile_match_body(&arm.body)?;
2060                                self.end_scope();
2061                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
2062                            }
2063
2064                            let type_fail_target = self.chunk.code.len();
2065                            self.chunk.emit(Op::Pop, self.line);
2066                            let next_arm_jump = self.chunk.emit_jump(Op::Jump, self.line);
2067                            let scoped_fail_target = self.chunk.code.len();
2068                            self.chunk.emit(Op::PopScope, self.line);
2069                            self.chunk.emit(Op::Pop, self.line);
2070                            let next_arm_target = self.chunk.code.len();
2071
2072                            for skip in constraint_skips {
2073                                self.chunk.patch_jump_to(skip, scoped_fail_target);
2074                            }
2075                            self.chunk.patch_jump_to(skip_type, type_fail_target);
2076                            self.chunk.patch_jump_to(next_arm_jump, next_arm_target);
2077                        }
2078                        // List pattern: [literal, binding, ...]
2079                        Node::ListLiteral(elements) => {
2080                            self.chunk.emit(Op::Dup, self.line);
2081                            let typeof_idx =
2082                                self.chunk.add_constant(Constant::String("type_of".into()));
2083                            self.chunk.emit_u16(Op::Constant, typeof_idx, self.line);
2084                            self.chunk.emit(Op::Swap, self.line);
2085                            self.chunk.emit_u8(Op::Call, 1, self.line);
2086                            let list_str = self.chunk.add_constant(Constant::String("list".into()));
2087                            self.chunk.emit_u16(Op::Constant, list_str, self.line);
2088                            self.chunk.emit(Op::Equal, self.line);
2089                            let skip_type = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2090                            self.chunk.emit(Op::Pop, self.line);
2091
2092                            self.chunk.emit(Op::Dup, self.line);
2093                            let len_idx = self.chunk.add_constant(Constant::String("len".into()));
2094                            self.chunk.emit_u16(Op::Constant, len_idx, self.line);
2095                            self.chunk.emit(Op::Swap, self.line);
2096                            self.chunk.emit_u8(Op::Call, 1, self.line);
2097                            let count = self
2098                                .chunk
2099                                .add_constant(Constant::Int(elements.len() as i64));
2100                            self.chunk.emit_u16(Op::Constant, count, self.line);
2101                            self.chunk.emit(Op::GreaterEqual, self.line);
2102                            let skip_len = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2103                            self.chunk.emit(Op::Pop, self.line);
2104
2105                            let mut constraint_skips = Vec::new();
2106                            let mut bindings = Vec::new();
2107                            self.begin_scope();
2108                            for (i, elem) in elements.iter().enumerate() {
2109                                match &elem.node {
2110                                    Node::Identifier(name) if name != "_" => {
2111                                        bindings.push((i, name.clone()));
2112                                    }
2113                                    Node::Identifier(_) => {} // wildcard `_`
2114                                    _ => {
2115                                        self.chunk.emit(Op::Dup, self.line);
2116                                        let idx_const =
2117                                            self.chunk.add_constant(Constant::Int(i as i64));
2118                                        self.chunk.emit_u16(Op::Constant, idx_const, self.line);
2119                                        self.chunk.emit(Op::Subscript, self.line);
2120                                        self.compile_node(elem)?;
2121                                        self.chunk.emit(Op::Equal, self.line);
2122                                        let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2123                                        self.chunk.emit(Op::Pop, self.line);
2124                                        constraint_skips.push(skip);
2125                                    }
2126                                }
2127                            }
2128
2129                            for (i, name) in &bindings {
2130                                self.chunk.emit(Op::Dup, self.line);
2131                                let idx_const = self.chunk.add_constant(Constant::Int(*i as i64));
2132                                self.chunk.emit_u16(Op::Constant, idx_const, self.line);
2133                                self.chunk.emit(Op::Subscript, self.line);
2134                                let name_idx =
2135                                    self.chunk.add_constant(Constant::String(name.clone()));
2136                                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
2137                            }
2138
2139                            // Optional guard
2140                            if let Some(ref guard) = arm.guard {
2141                                self.compile_node(guard)?;
2142                                let guard_skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2143                                self.chunk.emit(Op::Pop, self.line);
2144                                self.chunk.emit(Op::Pop, self.line);
2145                                self.compile_match_body(&arm.body)?;
2146                                self.end_scope();
2147                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
2148                                self.chunk.patch_jump(guard_skip);
2149                                self.chunk.emit(Op::Pop, self.line);
2150                            } else {
2151                                self.chunk.emit(Op::Pop, self.line);
2152                                self.compile_match_body(&arm.body)?;
2153                                self.end_scope();
2154                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
2155                            }
2156
2157                            let pre_scope_fail_target = self.chunk.code.len();
2158                            self.chunk.emit(Op::Pop, self.line);
2159                            let next_arm_jump = self.chunk.emit_jump(Op::Jump, self.line);
2160                            let scoped_fail_target = self.chunk.code.len();
2161                            self.chunk.emit(Op::PopScope, self.line);
2162                            self.chunk.emit(Op::Pop, self.line);
2163                            let next_arm_target = self.chunk.code.len();
2164                            for skip in constraint_skips {
2165                                self.chunk.patch_jump_to(skip, scoped_fail_target);
2166                            }
2167                            self.chunk.patch_jump_to(skip_len, pre_scope_fail_target);
2168                            self.chunk.patch_jump_to(skip_type, pre_scope_fail_target);
2169                            self.chunk.patch_jump_to(next_arm_jump, next_arm_target);
2170                        }
2171                        // Literal/expression pattern — compare with Equal.
2172                        _ => {
2173                            self.chunk.emit(Op::Dup, self.line);
2174                            self.compile_node(&arm.pattern)?;
2175                            self.chunk.emit(Op::Equal, self.line);
2176                            let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2177                            self.chunk.emit(Op::Pop, self.line);
2178                            if let Some(ref guard) = arm.guard {
2179                                self.compile_node(guard)?;
2180                                let guard_skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2181                                self.chunk.emit(Op::Pop, self.line);
2182                                self.begin_scope();
2183                                self.chunk.emit(Op::Pop, self.line);
2184                                self.compile_match_body(&arm.body)?;
2185                                self.end_scope();
2186                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
2187                                self.chunk.patch_jump(guard_skip);
2188                                self.chunk.emit(Op::Pop, self.line);
2189                            } else {
2190                                self.begin_scope();
2191                                self.chunk.emit(Op::Pop, self.line);
2192                                self.compile_match_body(&arm.body)?;
2193                                self.end_scope();
2194                                end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
2195                            }
2196                            self.chunk.patch_jump(skip);
2197                            self.chunk.emit(Op::Pop, self.line);
2198                        }
2199                    }
2200                }
2201                let msg_idx = self.chunk.add_constant(Constant::String(
2202                    "No match arm matched the value".to_string(),
2203                ));
2204                self.chunk.emit(Op::Pop, self.line);
2205                self.chunk.emit_u16(Op::Constant, msg_idx, self.line);
2206                self.chunk.emit(Op::Throw, self.line);
2207                for j in end_jumps {
2208                    self.chunk.patch_jump(j);
2209                }
2210            }
2211
2212            Node::RangeExpr {
2213                start,
2214                end,
2215                inclusive,
2216            } => {
2217                let name_idx = self
2218                    .chunk
2219                    .add_constant(Constant::String("__range__".to_string()));
2220                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
2221                self.compile_node(start)?;
2222                self.compile_node(end)?;
2223                if *inclusive {
2224                    self.chunk.emit(Op::True, self.line);
2225                } else {
2226                    self.chunk.emit(Op::False, self.line);
2227                }
2228                self.chunk.emit_u8(Op::Call, 3, self.line);
2229            }
2230
2231            Node::GuardStmt {
2232                condition,
2233                else_body,
2234            } => {
2235                self.compile_node(condition)?;
2236                let skip_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
2237                self.chunk.emit(Op::Pop, self.line);
2238                self.compile_scoped_block(else_body)?;
2239                // Guard is a statement, not an expression: pop any trailing value.
2240                if !else_body.is_empty() && Self::produces_value(&else_body.last().unwrap().node) {
2241                    self.chunk.emit(Op::Pop, self.line);
2242                }
2243                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
2244                self.chunk.patch_jump(skip_jump);
2245                self.chunk.emit(Op::Pop, self.line);
2246                self.chunk.patch_jump(end_jump);
2247                self.chunk.emit(Op::Nil, self.line);
2248            }
2249
2250            Node::RequireStmt { condition, message } => {
2251                self.compile_node(condition)?;
2252                let ok_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
2253                self.chunk.emit(Op::Pop, self.line);
2254                if let Some(message) = message {
2255                    self.compile_node(message)?;
2256                } else {
2257                    let idx = self
2258                        .chunk
2259                        .add_constant(Constant::String("require condition failed".to_string()));
2260                    self.chunk.emit_u16(Op::Constant, idx, self.line);
2261                }
2262                self.chunk.emit(Op::Throw, self.line);
2263                self.chunk.patch_jump(ok_jump);
2264                self.chunk.emit(Op::Pop, self.line);
2265            }
2266
2267            Node::Block(stmts) => {
2268                self.compile_scoped_block(stmts)?;
2269            }
2270
2271            Node::DeadlineBlock { duration, body } => {
2272                self.compile_node(duration)?;
2273                self.chunk.emit(Op::DeadlineSetup, self.line);
2274                self.compile_scoped_block(body)?;
2275                self.chunk.emit(Op::DeadlineEnd, self.line);
2276            }
2277
2278            Node::MutexBlock { body } => {
2279                // v1: single-threaded, but still uses a lexical block scope.
2280                self.begin_scope();
2281                for sn in body {
2282                    self.compile_node(sn)?;
2283                    if Self::produces_value(&sn.node) {
2284                        self.chunk.emit(Op::Pop, self.line);
2285                    }
2286                }
2287                self.chunk.emit(Op::Nil, self.line);
2288                self.end_scope();
2289            }
2290
2291            Node::DeferStmt { body } => {
2292                // Push onto the finally stack so it runs on return/throw/scope-exit.
2293                self.finally_bodies
2294                    .push(FinallyEntry::Finally(body.clone()));
2295                self.chunk.emit(Op::Nil, self.line);
2296            }
2297
2298            Node::YieldExpr { value } => {
2299                if let Some(val) = value {
2300                    self.compile_node(val)?;
2301                } else {
2302                    self.chunk.emit(Op::Nil, self.line);
2303                }
2304                self.chunk.emit(Op::Yield, self.line);
2305            }
2306
2307            Node::EnumConstruct {
2308                enum_name,
2309                variant,
2310                args,
2311            } => {
2312                for arg in args {
2313                    self.compile_node(arg)?;
2314                }
2315                let enum_idx = self.chunk.add_constant(Constant::String(enum_name.clone()));
2316                let var_idx = self.chunk.add_constant(Constant::String(variant.clone()));
2317                // BuildEnum operands: enum_name_idx, variant_idx, field_count.
2318                self.chunk.emit_u16(Op::BuildEnum, enum_idx, self.line);
2319                let hi = (var_idx >> 8) as u8;
2320                let lo = var_idx as u8;
2321                self.chunk.code.push(hi);
2322                self.chunk.code.push(lo);
2323                self.chunk.lines.push(self.line);
2324                self.chunk.columns.push(self.column);
2325                self.chunk.lines.push(self.line);
2326                self.chunk.columns.push(self.column);
2327                let fc = args.len() as u16;
2328                let fhi = (fc >> 8) as u8;
2329                let flo = fc as u8;
2330                self.chunk.code.push(fhi);
2331                self.chunk.code.push(flo);
2332                self.chunk.lines.push(self.line);
2333                self.chunk.columns.push(self.column);
2334                self.chunk.lines.push(self.line);
2335                self.chunk.columns.push(self.column);
2336            }
2337
2338            Node::StructConstruct {
2339                struct_name,
2340                fields,
2341            } => {
2342                // Route through `__make_struct` so impl dispatch sees a StructInstance.
2343                let make_idx = self
2344                    .chunk
2345                    .add_constant(Constant::String("__make_struct".to_string()));
2346                let struct_name_idx = self
2347                    .chunk
2348                    .add_constant(Constant::String(struct_name.clone()));
2349                self.chunk.emit_u16(Op::Constant, make_idx, self.line);
2350                self.chunk
2351                    .emit_u16(Op::Constant, struct_name_idx, self.line);
2352
2353                for entry in fields {
2354                    self.compile_node(&entry.key)?;
2355                    self.compile_node(&entry.value)?;
2356                }
2357                self.chunk
2358                    .emit_u16(Op::BuildDict, fields.len() as u16, self.line);
2359                self.chunk.emit_u8(Op::Call, 2, self.line);
2360            }
2361
2362            Node::ImportDecl { path } => {
2363                let idx = self.chunk.add_constant(Constant::String(path.clone()));
2364                self.chunk.emit_u16(Op::Import, idx, self.line);
2365            }
2366
2367            Node::SelectiveImport { names, path } => {
2368                let path_idx = self.chunk.add_constant(Constant::String(path.clone()));
2369                let names_str = names.join(",");
2370                let names_idx = self.chunk.add_constant(Constant::String(names_str));
2371                self.chunk
2372                    .emit_u16(Op::SelectiveImport, path_idx, self.line);
2373                let hi = (names_idx >> 8) as u8;
2374                let lo = names_idx as u8;
2375                self.chunk.code.push(hi);
2376                self.chunk.code.push(lo);
2377                self.chunk.lines.push(self.line);
2378                self.chunk.columns.push(self.column);
2379                self.chunk.lines.push(self.line);
2380                self.chunk.columns.push(self.column);
2381            }
2382
2383            Node::TryOperator { operand } => {
2384                self.compile_node(operand)?;
2385                self.chunk.emit(Op::TryUnwrap, self.line);
2386            }
2387
2388            // `try* EXPR`: evaluate EXPR; on throw, run pending finally
2389            // blocks up to the innermost catch barrier and rethrow the
2390            // original value. On success, leave EXPR's value on the stack.
2391            //
2392            // Per the issue-#26 desugaring:
2393            //   { let _r = try { EXPR }
2394            //     guard is_ok(_r) else { throw unwrap_err(_r) }
2395            //     unwrap(_r) }
2396            //
2397            // The bytecode realizes this directly: install a try handler
2398            // around EXPR so a throw lands in our catch path, where we
2399            // pre-run pending finallys and re-emit `Throw`. Skipping the
2400            // intermediate Result.Ok/Err wrapping that `TryExpr` does
2401            // keeps the success path a no-op (operand value passes through
2402            // as-is).
2403            Node::TryStar { operand } => {
2404                if self.module_level {
2405                    return Err(CompileError {
2406                        message: "try* requires an enclosing function (fn, tool, or pipeline) so the rethrow has a target".into(),
2407                        line: self.line,
2408                    });
2409                }
2410                self.handler_depth += 1;
2411                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
2412                let empty_type = self.chunk.add_constant(Constant::String(String::new()));
2413                self.emit_type_name_extra(empty_type);
2414
2415                self.compile_node(operand)?;
2416
2417                self.handler_depth -= 1;
2418                self.chunk.emit(Op::PopHandler, self.line);
2419                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
2420
2421                // Catch path: thrown value is on the stack. Pre-run any
2422                // finallys between us and the innermost catch barrier
2423                // (mirrors `Node::ThrowStmt` lowering), then rethrow.
2424                self.chunk.patch_jump(catch_jump);
2425                let pending = self.pending_finallys_until_barrier();
2426                if pending.is_empty() {
2427                    self.chunk.emit(Op::Throw, self.line);
2428                } else {
2429                    self.temp_counter += 1;
2430                    let temp_name = format!("__try_star_err_{}__", self.temp_counter);
2431                    let save_idx = self.chunk.add_constant(Constant::String(temp_name.clone()));
2432                    self.chunk.emit_u16(Op::DefVar, save_idx, self.line);
2433                    for fb in &pending {
2434                        self.compile_finally_inline(fb)?;
2435                    }
2436                    let restore_idx = self.chunk.add_constant(Constant::String(temp_name));
2437                    self.chunk.emit_u16(Op::GetVar, restore_idx, self.line);
2438                    self.chunk.emit(Op::Throw, self.line);
2439                }
2440
2441                self.chunk.patch_jump(end_jump);
2442            }
2443
2444            Node::ImplBlock { type_name, methods } => {
2445                // Lower into a `__impl_TypeName` dict of name -> closure.
2446                for method_sn in methods {
2447                    if let Node::FnDecl {
2448                        name, params, body, ..
2449                    } = &method_sn.node
2450                    {
2451                        let key_idx = self.chunk.add_constant(Constant::String(name.clone()));
2452                        self.chunk.emit_u16(Op::Constant, key_idx, self.line);
2453
2454                        let mut fn_compiler = Compiler::for_nested_body();
2455                        fn_compiler.enum_names = self.enum_names.clone();
2456                        fn_compiler.emit_default_preamble(params)?;
2457                        fn_compiler.emit_type_checks(params);
2458                        fn_compiler.compile_block(body)?;
2459                        fn_compiler.chunk.emit(Op::Nil, self.line);
2460                        fn_compiler.chunk.emit(Op::Return, self.line);
2461
2462                        let func = CompiledFunction {
2463                            name: format!("{}.{}", type_name, name),
2464                            params: TypedParam::names(params),
2465                            default_start: TypedParam::default_start(params),
2466                            chunk: fn_compiler.chunk,
2467                            is_generator: false,
2468                            has_rest_param: false,
2469                        };
2470                        let fn_idx = self.chunk.functions.len();
2471                        self.chunk.functions.push(func);
2472                        self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2473                    }
2474                }
2475                let method_count = methods
2476                    .iter()
2477                    .filter(|m| matches!(m.node, Node::FnDecl { .. }))
2478                    .count();
2479                self.chunk
2480                    .emit_u16(Op::BuildDict, method_count as u16, self.line);
2481                let impl_name = format!("__impl_{}", type_name);
2482                let name_idx = self.chunk.add_constant(Constant::String(impl_name));
2483                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
2484            }
2485
2486            Node::StructDecl { name, .. } => {
2487                // Emit a constructor: StructName({field: val, ...}) -> StructInstance.
2488                let mut fn_compiler = Compiler::for_nested_body();
2489                fn_compiler.enum_names = self.enum_names.clone();
2490                let params = vec![TypedParam::untyped("__fields")];
2491                fn_compiler.emit_default_preamble(&params)?;
2492
2493                let make_idx = fn_compiler
2494                    .chunk
2495                    .add_constant(Constant::String("__make_struct".into()));
2496                fn_compiler
2497                    .chunk
2498                    .emit_u16(Op::Constant, make_idx, self.line);
2499                let sname_idx = fn_compiler
2500                    .chunk
2501                    .add_constant(Constant::String(name.clone()));
2502                fn_compiler
2503                    .chunk
2504                    .emit_u16(Op::Constant, sname_idx, self.line);
2505                let fields_idx = fn_compiler
2506                    .chunk
2507                    .add_constant(Constant::String("__fields".into()));
2508                fn_compiler
2509                    .chunk
2510                    .emit_u16(Op::GetVar, fields_idx, self.line);
2511                fn_compiler.chunk.emit_u8(Op::Call, 2, self.line);
2512                fn_compiler.chunk.emit(Op::Return, self.line);
2513
2514                let func = CompiledFunction {
2515                    name: name.clone(),
2516                    params: TypedParam::names(&params),
2517                    default_start: None,
2518                    chunk: fn_compiler.chunk,
2519                    is_generator: false,
2520                    has_rest_param: false,
2521                };
2522                let fn_idx = self.chunk.functions.len();
2523                self.chunk.functions.push(func);
2524                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2525                let name_idx = self.chunk.add_constant(Constant::String(name.clone()));
2526                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
2527            }
2528
2529            // Metadata-only declarations (no runtime effect).
2530            Node::Pipeline { .. }
2531            | Node::OverrideDecl { .. }
2532            | Node::TypeDecl { .. }
2533            | Node::EnumDecl { .. }
2534            | Node::InterfaceDecl { .. } => {
2535                self.chunk.emit(Op::Nil, self.line);
2536            }
2537
2538            Node::TryCatch {
2539                body,
2540                error_var,
2541                error_type,
2542                catch_body,
2543                finally_body,
2544            } => {
2545                // Extract the type name for typed catch (e.g., "AppError")
2546                let type_name = error_type.as_ref().and_then(|te| {
2547                    if let harn_parser::TypeExpr::Named(name) = te {
2548                        Some(name.clone())
2549                    } else {
2550                        None
2551                    }
2552                });
2553
2554                let type_name_idx = if let Some(ref tn) = type_name {
2555                    self.chunk.add_constant(Constant::String(tn.clone()))
2556                } else {
2557                    self.chunk.add_constant(Constant::String(String::new()))
2558                };
2559
2560                let has_catch = !catch_body.is_empty() || error_var.is_some();
2561                let has_finally = finally_body.is_some();
2562
2563                if has_catch && has_finally {
2564                    let finally_body = finally_body.as_ref().unwrap();
2565                    // During the try body: install both the catch barrier
2566                    // (so throws don't pre-run finallys beyond our catch)
2567                    // and our finally (so return/break/continue in the
2568                    // body still run it). Order matters — barrier is below
2569                    // our finally so pre-running stops *at* the barrier.
2570                    self.finally_bodies.push(FinallyEntry::CatchBarrier);
2571                    self.finally_bodies
2572                        .push(FinallyEntry::Finally(finally_body.clone()));
2573
2574                    self.handler_depth += 1;
2575                    let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
2576                    self.emit_type_name_extra(type_name_idx);
2577
2578                    self.compile_try_body(body)?;
2579
2580                    self.handler_depth -= 1;
2581                    self.chunk.emit(Op::PopHandler, self.line);
2582                    // Body-success path: throw never fired, so pre-run did
2583                    // not happen. Run finally now.
2584                    self.compile_finally_inline(finally_body)?;
2585                    // Drop both finally and barrier — we're leaving the
2586                    // try body; the catch handler compiles without them.
2587                    self.finally_bodies.pop(); // Finally
2588                    self.finally_bodies.pop(); // CatchBarrier
2589                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
2590
2591                    self.chunk.patch_jump(catch_jump);
2592                    self.begin_scope();
2593                    self.compile_catch_binding(error_var)?;
2594
2595                    // Inner try around catch body so a catch-body throw
2596                    // lands in our `rethrow_jump` and we emit a plain
2597                    // rethrow (finally already fired via the body's throw).
2598                    self.handler_depth += 1;
2599                    let rethrow_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
2600                    let empty_type = self.chunk.add_constant(Constant::String(String::new()));
2601                    self.emit_type_name_extra(empty_type);
2602
2603                    self.compile_try_body(catch_body)?;
2604
2605                    self.handler_depth -= 1;
2606                    self.chunk.emit(Op::PopHandler, self.line);
2607                    self.end_scope();
2608                    let end_jump2 = self.chunk.emit_jump(Op::Jump, self.line);
2609
2610                    // Rethrow handler: plain rethrow; finally already pre-ran
2611                    // via the body's Throw lowering before the outer handler
2612                    // delivered control into catch.
2613                    self.chunk.patch_jump(rethrow_jump);
2614                    self.compile_plain_rethrow()?;
2615                    self.end_scope();
2616
2617                    self.chunk.patch_jump(end_jump);
2618                    self.chunk.patch_jump(end_jump2);
2619                } else if has_finally {
2620                    let finally_body = finally_body.as_ref().unwrap();
2621                    // No catch: throws in the body unwind through us, so
2622                    // we don't install a barrier — our finally and any
2623                    // outer finallys are on the throw's escape path.
2624                    self.finally_bodies
2625                        .push(FinallyEntry::Finally(finally_body.clone()));
2626
2627                    self.handler_depth += 1;
2628                    let error_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
2629                    let empty_type = self.chunk.add_constant(Constant::String(String::new()));
2630                    self.emit_type_name_extra(empty_type);
2631
2632                    self.compile_try_body(body)?;
2633
2634                    self.handler_depth -= 1;
2635                    self.chunk.emit(Op::PopHandler, self.line);
2636                    self.compile_finally_inline(finally_body)?;
2637                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
2638
2639                    // Error path: save error, re-throw. Finally already
2640                    // pre-ran via the body's Throw lowering.
2641                    self.chunk.patch_jump(error_jump);
2642                    self.compile_plain_rethrow()?;
2643
2644                    self.chunk.patch_jump(end_jump);
2645
2646                    self.finally_bodies.pop(); // Finally
2647                } else {
2648                    // try-catch without finally: install a barrier so
2649                    // throws in the body don't pre-run outer finallys
2650                    // (the throw is caught here and won't unwind past).
2651                    self.finally_bodies.push(FinallyEntry::CatchBarrier);
2652
2653                    self.handler_depth += 1;
2654                    let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
2655                    self.emit_type_name_extra(type_name_idx);
2656
2657                    self.compile_try_body(body)?;
2658
2659                    self.handler_depth -= 1;
2660                    self.chunk.emit(Op::PopHandler, self.line);
2661                    self.finally_bodies.pop(); // CatchBarrier
2662                    let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
2663
2664                    self.chunk.patch_jump(catch_jump);
2665                    self.begin_scope();
2666                    self.compile_catch_binding(error_var)?;
2667
2668                    self.compile_try_body(catch_body)?;
2669                    self.end_scope();
2670
2671                    self.chunk.patch_jump(end_jump);
2672                }
2673            }
2674
2675            Node::TryExpr { body } => {
2676                // `try { body }` returns Result.Ok(value) or Result.Err(error).
2677                self.handler_depth += 1;
2678                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
2679                let empty_type = self.chunk.add_constant(Constant::String(String::new()));
2680                self.emit_type_name_extra(empty_type);
2681
2682                self.compile_try_body(body)?;
2683
2684                self.handler_depth -= 1;
2685                self.chunk.emit(Op::PopHandler, self.line);
2686
2687                // Wrap success in Result.Ok.
2688                let ok_idx = self.chunk.add_constant(Constant::String("Ok".to_string()));
2689                self.chunk.emit_u16(Op::Constant, ok_idx, self.line);
2690                self.chunk.emit(Op::Swap, self.line);
2691                self.chunk.emit_u8(Op::Call, 1, self.line);
2692
2693                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
2694
2695                // Error path: wrap in Result.Err.
2696                self.chunk.patch_jump(catch_jump);
2697
2698                let err_idx = self.chunk.add_constant(Constant::String("Err".to_string()));
2699                self.chunk.emit_u16(Op::Constant, err_idx, self.line);
2700                self.chunk.emit(Op::Swap, self.line);
2701                self.chunk.emit_u8(Op::Call, 1, self.line);
2702
2703                self.chunk.patch_jump(end_jump);
2704            }
2705
2706            Node::Retry { count, body } => {
2707                self.compile_node(count)?;
2708                let counter_name = "__retry_counter__";
2709                let counter_idx = self
2710                    .chunk
2711                    .add_constant(Constant::String(counter_name.to_string()));
2712                self.chunk.emit_u16(Op::DefVar, counter_idx, self.line);
2713
2714                // Store last error for re-throwing after retries are exhausted.
2715                self.chunk.emit(Op::Nil, self.line);
2716                let err_name = "__retry_last_error__";
2717                let err_idx = self
2718                    .chunk
2719                    .add_constant(Constant::String(err_name.to_string()));
2720                self.chunk.emit_u16(Op::DefVar, err_idx, self.line);
2721
2722                let loop_start = self.chunk.current_offset();
2723
2724                let catch_jump = self.chunk.emit_jump(Op::TryCatchSetup, self.line);
2725                // Empty type name → untyped catch.
2726                let empty_type = self.chunk.add_constant(Constant::String(String::new()));
2727                let hi = (empty_type >> 8) as u8;
2728                let lo = empty_type as u8;
2729                self.chunk.code.push(hi);
2730                self.chunk.code.push(lo);
2731                self.chunk.lines.push(self.line);
2732                self.chunk.columns.push(self.column);
2733                self.chunk.lines.push(self.line);
2734                self.chunk.columns.push(self.column);
2735
2736                self.compile_block(body)?;
2737
2738                self.chunk.emit(Op::PopHandler, self.line);
2739                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
2740
2741                self.chunk.patch_jump(catch_jump);
2742                self.chunk.emit(Op::Dup, self.line);
2743                self.chunk.emit_u16(Op::SetVar, err_idx, self.line);
2744                self.chunk.emit(Op::Pop, self.line);
2745
2746                self.chunk.emit_u16(Op::GetVar, counter_idx, self.line);
2747                let one_idx = self.chunk.add_constant(Constant::Int(1));
2748                self.chunk.emit_u16(Op::Constant, one_idx, self.line);
2749                self.chunk.emit(Op::Sub, self.line);
2750                self.chunk.emit(Op::Dup, self.line);
2751                self.chunk.emit_u16(Op::SetVar, counter_idx, self.line);
2752
2753                let zero_idx = self.chunk.add_constant(Constant::Int(0));
2754                self.chunk.emit_u16(Op::Constant, zero_idx, self.line);
2755                self.chunk.emit(Op::Greater, self.line);
2756                let retry_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2757                self.chunk.emit(Op::Pop, self.line);
2758                self.chunk.emit_u16(Op::Jump, loop_start as u16, self.line);
2759
2760                // Retries exhausted — re-throw the last error.
2761                self.chunk.patch_jump(retry_jump);
2762                self.chunk.emit(Op::Pop, self.line);
2763                self.chunk.emit_u16(Op::GetVar, err_idx, self.line);
2764                self.chunk.emit(Op::Throw, self.line);
2765
2766                self.chunk.patch_jump(end_jump);
2767                self.chunk.emit(Op::Nil, self.line);
2768            }
2769
2770            Node::Parallel {
2771                mode,
2772                expr,
2773                variable,
2774                body,
2775                options,
2776            } => {
2777                // Push the `max_concurrent` cap first so the runtime
2778                // opcodes can pop it beneath the iterable + closure. A
2779                // cap of 0 (or a missing `with { ... }` clause) means
2780                // "unlimited". Unknown option keys are parser errors.
2781                let cap_expr = options
2782                    .iter()
2783                    .find(|(key, _)| key == "max_concurrent")
2784                    .map(|(_, value)| value);
2785                if let Some(cap_expr) = cap_expr {
2786                    self.compile_node(cap_expr)?;
2787                } else {
2788                    let zero_idx = self.chunk.add_constant(Constant::Int(0));
2789                    self.chunk.emit_u16(Op::Constant, zero_idx, self.line);
2790                }
2791                self.compile_node(expr)?;
2792                let mut fn_compiler = Compiler::for_nested_body();
2793                fn_compiler.enum_names = self.enum_names.clone();
2794                fn_compiler.compile_block(body)?;
2795                fn_compiler.chunk.emit(Op::Return, self.line);
2796                let (fn_name, params) = match mode {
2797                    ParallelMode::Count => (
2798                        "<parallel>",
2799                        vec![variable.clone().unwrap_or_else(|| "__i__".to_string())],
2800                    ),
2801                    ParallelMode::Each => (
2802                        "<parallel_each>",
2803                        vec![variable.clone().unwrap_or_else(|| "__item__".to_string())],
2804                    ),
2805                    ParallelMode::Settle => (
2806                        "<parallel_settle>",
2807                        vec![variable.clone().unwrap_or_else(|| "__item__".to_string())],
2808                    ),
2809                };
2810                let func = CompiledFunction {
2811                    name: fn_name.to_string(),
2812                    params,
2813                    default_start: None,
2814                    chunk: fn_compiler.chunk,
2815                    is_generator: false,
2816                    has_rest_param: false,
2817                };
2818                let fn_idx = self.chunk.functions.len();
2819                self.chunk.functions.push(func);
2820                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2821                let op = match mode {
2822                    ParallelMode::Count => Op::Parallel,
2823                    ParallelMode::Each => Op::ParallelMap,
2824                    ParallelMode::Settle => Op::ParallelSettle,
2825                };
2826                self.chunk.emit(op, self.line);
2827            }
2828
2829            Node::SpawnExpr { body } => {
2830                let mut fn_compiler = Compiler::for_nested_body();
2831                fn_compiler.enum_names = self.enum_names.clone();
2832                fn_compiler.compile_block(body)?;
2833                fn_compiler.chunk.emit(Op::Return, self.line);
2834                let func = CompiledFunction {
2835                    name: "<spawn>".to_string(),
2836                    params: vec![],
2837                    default_start: None,
2838                    chunk: fn_compiler.chunk,
2839                    is_generator: false,
2840                    has_rest_param: false,
2841                };
2842                let fn_idx = self.chunk.functions.len();
2843                self.chunk.functions.push(func);
2844                self.chunk.emit_u16(Op::Closure, fn_idx as u16, self.line);
2845                self.chunk.emit(Op::Spawn, self.line);
2846            }
2847            Node::SelectExpr {
2848                cases,
2849                timeout,
2850                default_body,
2851            } => {
2852                // Desugar `select` into a builtin call returning a dict with
2853                // {index, value}, then dispatch on result.index. `index == -1`
2854                // means timeout / default fell through.
2855                let builtin_name = if timeout.is_some() {
2856                    "__select_timeout"
2857                } else if default_body.is_some() {
2858                    "__select_try"
2859                } else {
2860                    "__select_list"
2861                };
2862
2863                let name_idx = self
2864                    .chunk
2865                    .add_constant(Constant::String(builtin_name.into()));
2866                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
2867
2868                for case in cases {
2869                    self.compile_node(&case.channel)?;
2870                }
2871                self.chunk
2872                    .emit_u16(Op::BuildList, cases.len() as u16, self.line);
2873
2874                if let Some((duration_expr, _)) = timeout {
2875                    self.compile_node(duration_expr)?;
2876                    self.chunk.emit_u8(Op::Call, 2, self.line);
2877                } else {
2878                    self.chunk.emit_u8(Op::Call, 1, self.line);
2879                }
2880
2881                self.temp_counter += 1;
2882                let result_name = format!("__sel_result_{}__", self.temp_counter);
2883                let result_idx = self
2884                    .chunk
2885                    .add_constant(Constant::String(result_name.clone()));
2886                self.chunk.emit_u16(Op::DefVar, result_idx, self.line);
2887
2888                let mut end_jumps = Vec::new();
2889
2890                for (i, case) in cases.iter().enumerate() {
2891                    let get_r = self
2892                        .chunk
2893                        .add_constant(Constant::String(result_name.clone()));
2894                    self.chunk.emit_u16(Op::GetVar, get_r, self.line);
2895                    let idx_prop = self.chunk.add_constant(Constant::String("index".into()));
2896                    self.chunk.emit_u16(Op::GetProperty, idx_prop, self.line);
2897                    let case_i = self.chunk.add_constant(Constant::Int(i as i64));
2898                    self.chunk.emit_u16(Op::Constant, case_i, self.line);
2899                    self.chunk.emit(Op::Equal, self.line);
2900                    let skip = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
2901                    self.chunk.emit(Op::Pop, self.line);
2902                    self.begin_scope();
2903
2904                    let get_r2 = self
2905                        .chunk
2906                        .add_constant(Constant::String(result_name.clone()));
2907                    self.chunk.emit_u16(Op::GetVar, get_r2, self.line);
2908                    let val_prop = self.chunk.add_constant(Constant::String("value".into()));
2909                    self.chunk.emit_u16(Op::GetProperty, val_prop, self.line);
2910                    let var_idx = self
2911                        .chunk
2912                        .add_constant(Constant::String(case.variable.clone()));
2913                    self.chunk.emit_u16(Op::DefLet, var_idx, self.line);
2914
2915                    self.compile_try_body(&case.body)?;
2916                    self.end_scope();
2917                    end_jumps.push(self.chunk.emit_jump(Op::Jump, self.line));
2918                    self.chunk.patch_jump(skip);
2919                    self.chunk.emit(Op::Pop, self.line);
2920                }
2921
2922                if let Some((_, ref timeout_body)) = timeout {
2923                    self.compile_try_body(timeout_body)?;
2924                } else if let Some(ref def_body) = default_body {
2925                    self.compile_try_body(def_body)?;
2926                } else {
2927                    self.chunk.emit(Op::Nil, self.line);
2928                }
2929
2930                for ej in end_jumps {
2931                    self.chunk.patch_jump(ej);
2932                }
2933            }
2934            Node::Spread(_) => {
2935                return Err(CompileError {
2936                    message: "spread (...) can only be used inside list literals, dict literals, or function call arguments".into(),
2937                    line: self.line,
2938                });
2939            }
2940        }
2941        Ok(())
2942    }
2943
2944    /// Compile a destructuring binding pattern.
2945    /// Expects the RHS value to already be on the stack.
2946    /// After this, the value is consumed (popped) and each binding is defined.
2947    fn compile_destructuring(
2948        &mut self,
2949        pattern: &BindingPattern,
2950        is_mutable: bool,
2951    ) -> Result<(), CompileError> {
2952        let def_op = if is_mutable { Op::DefVar } else { Op::DefLet };
2953        match pattern {
2954            BindingPattern::Identifier(name) => {
2955                let idx = self.chunk.add_constant(Constant::String(name.clone()));
2956                self.chunk.emit_u16(def_op, idx, self.line);
2957            }
2958            BindingPattern::Dict(fields) => {
2959                // Runtime `__assert_dict(value)` type check on the RHS.
2960                self.chunk.emit(Op::Dup, self.line);
2961                let assert_idx = self
2962                    .chunk
2963                    .add_constant(Constant::String("__assert_dict".into()));
2964                self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
2965                self.chunk.emit(Op::Swap, self.line);
2966                self.chunk.emit_u8(Op::Call, 1, self.line);
2967                self.chunk.emit(Op::Pop, self.line);
2968
2969                let non_rest: Vec<_> = fields.iter().filter(|f| !f.is_rest).collect();
2970                let rest_field = fields.iter().find(|f| f.is_rest);
2971
2972                for field in &non_rest {
2973                    self.chunk.emit(Op::Dup, self.line);
2974                    let key_idx = self.chunk.add_constant(Constant::String(field.key.clone()));
2975                    self.chunk.emit_u16(Op::Constant, key_idx, self.line);
2976                    self.chunk.emit(Op::Subscript, self.line);
2977                    if let Some(default_expr) = &field.default_value {
2978                        // Nil-coalescing: use default when the field was nil.
2979                        self.chunk.emit(Op::Dup, self.line);
2980                        self.chunk.emit(Op::Nil, self.line);
2981                        self.chunk.emit(Op::NotEqual, self.line);
2982                        let skip_default = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
2983                        self.chunk.emit(Op::Pop, self.line);
2984                        self.chunk.emit(Op::Pop, self.line);
2985                        self.compile_node(default_expr)?;
2986                        let end = self.chunk.emit_jump(Op::Jump, self.line);
2987                        self.chunk.patch_jump(skip_default);
2988                        self.chunk.emit(Op::Pop, self.line);
2989                        self.chunk.patch_jump(end);
2990                    }
2991                    let binding_name = field.alias.as_deref().unwrap_or(&field.key);
2992                    let name_idx = self
2993                        .chunk
2994                        .add_constant(Constant::String(binding_name.to_string()));
2995                    self.chunk.emit_u16(def_op, name_idx, self.line);
2996                }
2997
2998                if let Some(rest) = rest_field {
2999                    // `__dict_rest(dict, [keys_to_exclude])`.
3000                    let fn_idx = self
3001                        .chunk
3002                        .add_constant(Constant::String("__dict_rest".into()));
3003                    self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
3004                    self.chunk.emit(Op::Swap, self.line);
3005                    for field in &non_rest {
3006                        let key_idx = self.chunk.add_constant(Constant::String(field.key.clone()));
3007                        self.chunk.emit_u16(Op::Constant, key_idx, self.line);
3008                    }
3009                    self.chunk
3010                        .emit_u16(Op::BuildList, non_rest.len() as u16, self.line);
3011                    self.chunk.emit_u8(Op::Call, 2, self.line);
3012                    let rest_name = &rest.key;
3013                    let rest_idx = self.chunk.add_constant(Constant::String(rest_name.clone()));
3014                    self.chunk.emit_u16(def_op, rest_idx, self.line);
3015                } else {
3016                    self.chunk.emit(Op::Pop, self.line);
3017                }
3018            }
3019            BindingPattern::Pair(first_name, second_name) => {
3020                self.chunk.emit(Op::Dup, self.line);
3021                let first_key_idx = self
3022                    .chunk
3023                    .add_constant(Constant::String("first".to_string()));
3024                self.chunk
3025                    .emit_u16(Op::GetProperty, first_key_idx, self.line);
3026                let first_name_idx = self
3027                    .chunk
3028                    .add_constant(Constant::String(first_name.clone()));
3029                self.chunk.emit_u16(def_op, first_name_idx, self.line);
3030
3031                let second_key_idx = self
3032                    .chunk
3033                    .add_constant(Constant::String("second".to_string()));
3034                self.chunk
3035                    .emit_u16(Op::GetProperty, second_key_idx, self.line);
3036                let second_name_idx = self
3037                    .chunk
3038                    .add_constant(Constant::String(second_name.clone()));
3039                self.chunk.emit_u16(def_op, second_name_idx, self.line);
3040                // No trailing Pop: GetProperty consumed the source pair.
3041            }
3042            BindingPattern::List(elements) => {
3043                // Runtime `__assert_list(value)` type check on the RHS.
3044                self.chunk.emit(Op::Dup, self.line);
3045                let assert_idx = self
3046                    .chunk
3047                    .add_constant(Constant::String("__assert_list".into()));
3048                self.chunk.emit_u16(Op::Constant, assert_idx, self.line);
3049                self.chunk.emit(Op::Swap, self.line);
3050                self.chunk.emit_u8(Op::Call, 1, self.line);
3051                self.chunk.emit(Op::Pop, self.line);
3052
3053                let non_rest: Vec<_> = elements.iter().filter(|e| !e.is_rest).collect();
3054                let rest_elem = elements.iter().find(|e| e.is_rest);
3055
3056                for (i, elem) in non_rest.iter().enumerate() {
3057                    self.chunk.emit(Op::Dup, self.line);
3058                    let idx_const = self.chunk.add_constant(Constant::Int(i as i64));
3059                    self.chunk.emit_u16(Op::Constant, idx_const, self.line);
3060                    self.chunk.emit(Op::Subscript, self.line);
3061                    if let Some(default_expr) = &elem.default_value {
3062                        // Nil-coalescing: use default when the slot was nil.
3063                        self.chunk.emit(Op::Dup, self.line);
3064                        self.chunk.emit(Op::Nil, self.line);
3065                        self.chunk.emit(Op::NotEqual, self.line);
3066                        let skip_default = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
3067                        self.chunk.emit(Op::Pop, self.line);
3068                        self.chunk.emit(Op::Pop, self.line);
3069                        self.compile_node(default_expr)?;
3070                        let end = self.chunk.emit_jump(Op::Jump, self.line);
3071                        self.chunk.patch_jump(skip_default);
3072                        self.chunk.emit(Op::Pop, self.line);
3073                        self.chunk.patch_jump(end);
3074                    }
3075                    let name_idx = self.chunk.add_constant(Constant::String(elem.name.clone()));
3076                    self.chunk.emit_u16(def_op, name_idx, self.line);
3077                }
3078
3079                if let Some(rest) = rest_elem {
3080                    // Slice list[n..] where n = non_rest.len(); Slice expects
3081                    // object, start, end on the stack.
3082                    let start_idx = self
3083                        .chunk
3084                        .add_constant(Constant::Int(non_rest.len() as i64));
3085                    self.chunk.emit_u16(Op::Constant, start_idx, self.line);
3086                    self.chunk.emit(Op::Nil, self.line);
3087                    self.chunk.emit(Op::Slice, self.line);
3088                    let rest_name_idx =
3089                        self.chunk.add_constant(Constant::String(rest.name.clone()));
3090                    self.chunk.emit_u16(def_op, rest_name_idx, self.line);
3091                } else {
3092                    self.chunk.emit(Op::Pop, self.line);
3093                }
3094            }
3095        }
3096        Ok(())
3097    }
3098
3099    /// Check if a node produces a value on the stack that needs to be popped.
3100    fn produces_value(node: &Node) -> bool {
3101        match node {
3102            Node::LetBinding { .. }
3103            | Node::VarBinding { .. }
3104            | Node::Assignment { .. }
3105            | Node::ReturnStmt { .. }
3106            | Node::FnDecl { .. }
3107            | Node::ToolDecl { .. }
3108            | Node::ImplBlock { .. }
3109            | Node::StructDecl { .. }
3110            | Node::EnumDecl { .. }
3111            | Node::InterfaceDecl { .. }
3112            | Node::TypeDecl { .. }
3113            | Node::ThrowStmt { .. }
3114            | Node::BreakStmt
3115            | Node::ContinueStmt
3116            | Node::RequireStmt { .. }
3117            | Node::DeferStmt { .. } => false,
3118            Node::TryCatch { .. }
3119            | Node::TryExpr { .. }
3120            | Node::Retry { .. }
3121            | Node::GuardStmt { .. }
3122            | Node::DeadlineBlock { .. }
3123            | Node::MutexBlock { .. }
3124            | Node::Spread(_) => true,
3125            _ => true,
3126        }
3127    }
3128}
3129
3130impl Compiler {
3131    /// Compile a function body into a CompiledFunction (for import support).
3132    ///
3133    /// This path is used when a module is imported and its top-level `fn`
3134    /// declarations are loaded into the importer's environment. It MUST emit
3135    /// the same function preamble as the in-file `Node::FnDecl` path, or
3136    /// imported functions will behave differently from locally-defined ones —
3137    /// in particular, default parameter values would never be set and typed
3138    /// parameters would not be runtime-checked.
3139    ///
3140    /// `source_file`, when provided, tags the resulting chunk so runtime
3141    /// errors can attribute frames to the imported file rather than the
3142    /// entry-point pipeline.
3143    pub fn compile_fn_body(
3144        &mut self,
3145        params: &[TypedParam],
3146        body: &[SNode],
3147        source_file: Option<String>,
3148    ) -> Result<CompiledFunction, CompileError> {
3149        let mut fn_compiler = Compiler::for_nested_body();
3150        fn_compiler.enum_names = self.enum_names.clone();
3151        fn_compiler.emit_default_preamble(params)?;
3152        fn_compiler.emit_type_checks(params);
3153        let is_gen = body_contains_yield(body);
3154        fn_compiler.compile_block(body)?;
3155        fn_compiler.chunk.emit(Op::Nil, 0);
3156        fn_compiler.chunk.emit(Op::Return, 0);
3157        fn_compiler.chunk.source_file = source_file;
3158        Ok(CompiledFunction {
3159            name: String::new(),
3160            params: TypedParam::names(params),
3161            default_start: TypedParam::default_start(params),
3162            chunk: fn_compiler.chunk,
3163            is_generator: is_gen,
3164            has_rest_param: false,
3165        })
3166    }
3167
3168    /// Compile a match arm body, ensuring it always pushes exactly one value.
3169    fn compile_match_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
3170        self.begin_scope();
3171        if body.is_empty() {
3172            self.chunk.emit(Op::Nil, self.line);
3173        } else {
3174            self.compile_block(body)?;
3175            if !Self::produces_value(&body.last().unwrap().node) {
3176                self.chunk.emit(Op::Nil, self.line);
3177            }
3178        }
3179        self.end_scope();
3180        Ok(())
3181    }
3182
3183    /// Emit the binary op instruction for a compound assignment operator.
3184    fn emit_compound_op(&mut self, op: &str) -> Result<(), CompileError> {
3185        match op {
3186            "+" => self.chunk.emit(Op::Add, self.line),
3187            "-" => self.chunk.emit(Op::Sub, self.line),
3188            "*" => self.chunk.emit(Op::Mul, self.line),
3189            "/" => self.chunk.emit(Op::Div, self.line),
3190            "%" => self.chunk.emit(Op::Mod, self.line),
3191            _ => {
3192                return Err(CompileError {
3193                    message: format!("Unknown compound operator: {op}"),
3194                    line: self.line,
3195                })
3196            }
3197        }
3198        Ok(())
3199    }
3200
3201    /// Extract the root variable name from a (possibly nested) access expression.
3202    fn root_var_name(&self, node: &SNode) -> Option<String> {
3203        match &node.node {
3204            Node::Identifier(name) => Some(name.clone()),
3205            Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
3206                self.root_var_name(object)
3207            }
3208            Node::SubscriptAccess { object, .. } => self.root_var_name(object),
3209            _ => None,
3210        }
3211    }
3212
3213    fn compile_top_level_declarations(&mut self, program: &[SNode]) -> Result<(), CompileError> {
3214        // Phase 1: evaluate module-level `let` / `var` bindings first, in
3215        // source order. This ensures function closures compiled in phase 2
3216        // capture these names in their env snapshot via `Op::Closure` —
3217        // fixing the "Undefined variable: FOO" surprise where a top-level
3218        // `let FOO = "..."` was silently dropped because it wasn't in this
3219        // match list. Keep in step with the import-time init path in
3220        // `crates/harn-vm/src/vm/imports.rs` (`module_state` construction).
3221        for sn in program {
3222            if matches!(&sn.node, Node::LetBinding { .. } | Node::VarBinding { .. }) {
3223                self.compile_node(sn)?;
3224            }
3225        }
3226        // Phase 2: compile type and function declarations. Function closures
3227        // created here capture the current env which now includes the
3228        // module-level bindings from phase 1.
3229        for sn in program {
3230            if matches!(
3231                &sn.node,
3232                Node::FnDecl { .. }
3233                    | Node::ToolDecl { .. }
3234                    | Node::ImplBlock { .. }
3235                    | Node::StructDecl { .. }
3236                    | Node::EnumDecl { .. }
3237                    | Node::InterfaceDecl { .. }
3238                    | Node::TypeDecl { .. }
3239            ) {
3240                self.compile_node(sn)?;
3241            }
3242        }
3243        Ok(())
3244    }
3245}
3246
3247impl Compiler {
3248    /// Recursively collect all enum type names from the AST.
3249    fn collect_enum_names(nodes: &[SNode], names: &mut std::collections::HashSet<String>) {
3250        for sn in nodes {
3251            match &sn.node {
3252                Node::EnumDecl { name, .. } => {
3253                    names.insert(name.clone());
3254                }
3255                Node::Pipeline { body, .. } => {
3256                    Self::collect_enum_names(body, names);
3257                }
3258                Node::FnDecl { body, .. } | Node::ToolDecl { body, .. } => {
3259                    Self::collect_enum_names(body, names);
3260                }
3261                Node::Block(stmts) => {
3262                    Self::collect_enum_names(stmts, names);
3263                }
3264                _ => {}
3265            }
3266        }
3267    }
3268
3269    fn collect_interface_methods(
3270        nodes: &[SNode],
3271        interfaces: &mut std::collections::HashMap<String, Vec<String>>,
3272    ) {
3273        for sn in nodes {
3274            match &sn.node {
3275                Node::InterfaceDecl { name, methods, .. } => {
3276                    let method_names: Vec<String> =
3277                        methods.iter().map(|m| m.name.clone()).collect();
3278                    interfaces.insert(name.clone(), method_names);
3279                }
3280                Node::Pipeline { body, .. }
3281                | Node::FnDecl { body, .. }
3282                | Node::ToolDecl { body, .. } => {
3283                    Self::collect_interface_methods(body, interfaces);
3284                }
3285                Node::Block(stmts) => {
3286                    Self::collect_interface_methods(stmts, interfaces);
3287                }
3288                _ => {}
3289            }
3290        }
3291    }
3292}
3293
3294impl Default for Compiler {
3295    fn default() -> Self {
3296        Self::new()
3297    }
3298}
3299
3300/// Check if a list of AST nodes contains any `yield` expression (used to detect generator functions).
3301fn body_contains_yield(nodes: &[SNode]) -> bool {
3302    nodes.iter().any(|sn| node_contains_yield(&sn.node))
3303}
3304
3305fn node_contains_yield(node: &Node) -> bool {
3306    match node {
3307        Node::YieldExpr { .. } => true,
3308        // Don't recurse into nested fn/closure: yield in a nested fn does
3309        // NOT make the outer a generator.
3310        Node::FnDecl { .. } | Node::Closure { .. } => false,
3311        Node::Block(stmts) => body_contains_yield(stmts),
3312        Node::IfElse {
3313            condition,
3314            then_body,
3315            else_body,
3316        } => {
3317            node_contains_yield(&condition.node)
3318                || body_contains_yield(then_body)
3319                || else_body.as_ref().is_some_and(|b| body_contains_yield(b))
3320        }
3321        Node::WhileLoop { condition, body } => {
3322            node_contains_yield(&condition.node) || body_contains_yield(body)
3323        }
3324        Node::ForIn { iterable, body, .. } => {
3325            node_contains_yield(&iterable.node) || body_contains_yield(body)
3326        }
3327        Node::TryCatch {
3328            body, catch_body, ..
3329        } => body_contains_yield(body) || body_contains_yield(catch_body),
3330        Node::TryExpr { body } => body_contains_yield(body),
3331        _ => false,
3332    }
3333}
3334
3335/// Check if an AST node contains `_` identifier (pipe placeholder).
3336fn contains_pipe_placeholder(node: &SNode) -> bool {
3337    match &node.node {
3338        Node::Identifier(name) if name == "_" => true,
3339        Node::FunctionCall { args, .. } => args.iter().any(contains_pipe_placeholder),
3340        Node::MethodCall { object, args, .. } => {
3341            contains_pipe_placeholder(object) || args.iter().any(contains_pipe_placeholder)
3342        }
3343        Node::BinaryOp { left, right, .. } => {
3344            contains_pipe_placeholder(left) || contains_pipe_placeholder(right)
3345        }
3346        Node::UnaryOp { operand, .. } => contains_pipe_placeholder(operand),
3347        Node::ListLiteral(items) => items.iter().any(contains_pipe_placeholder),
3348        Node::PropertyAccess { object, .. } => contains_pipe_placeholder(object),
3349        Node::SubscriptAccess { object, index } => {
3350            contains_pipe_placeholder(object) || contains_pipe_placeholder(index)
3351        }
3352        _ => false,
3353    }
3354}
3355
3356/// Replace all `_` identifiers with `__pipe` in an AST node (for pipe placeholder desugaring).
3357fn replace_pipe_placeholder(node: &SNode) -> SNode {
3358    let new_node = match &node.node {
3359        Node::Identifier(name) if name == "_" => Node::Identifier("__pipe".into()),
3360        Node::FunctionCall { name, args } => Node::FunctionCall {
3361            name: name.clone(),
3362            args: args.iter().map(replace_pipe_placeholder).collect(),
3363        },
3364        Node::MethodCall {
3365            object,
3366            method,
3367            args,
3368        } => Node::MethodCall {
3369            object: Box::new(replace_pipe_placeholder(object)),
3370            method: method.clone(),
3371            args: args.iter().map(replace_pipe_placeholder).collect(),
3372        },
3373        Node::BinaryOp { op, left, right } => Node::BinaryOp {
3374            op: op.clone(),
3375            left: Box::new(replace_pipe_placeholder(left)),
3376            right: Box::new(replace_pipe_placeholder(right)),
3377        },
3378        Node::UnaryOp { op, operand } => Node::UnaryOp {
3379            op: op.clone(),
3380            operand: Box::new(replace_pipe_placeholder(operand)),
3381        },
3382        Node::ListLiteral(items) => {
3383            Node::ListLiteral(items.iter().map(replace_pipe_placeholder).collect())
3384        }
3385        Node::PropertyAccess { object, property } => Node::PropertyAccess {
3386            object: Box::new(replace_pipe_placeholder(object)),
3387            property: property.clone(),
3388        },
3389        Node::SubscriptAccess { object, index } => Node::SubscriptAccess {
3390            object: Box::new(replace_pipe_placeholder(object)),
3391            index: Box::new(replace_pipe_placeholder(index)),
3392        },
3393        _ => return node.clone(),
3394    };
3395    SNode::new(new_node, node.span)
3396}
3397
3398#[cfg(test)]
3399mod tests {
3400    use super::*;
3401    use harn_lexer::Lexer;
3402    use harn_parser::Parser;
3403
3404    fn compile_source(source: &str) -> Chunk {
3405        let mut lexer = Lexer::new(source);
3406        let tokens = lexer.tokenize().unwrap();
3407        let mut parser = Parser::new(tokens);
3408        let program = parser.parse().unwrap();
3409        Compiler::new().compile(&program).unwrap()
3410    }
3411
3412    #[test]
3413    fn test_compile_arithmetic() {
3414        let chunk = compile_source("pipeline test(task) { let x = 2 + 3 }");
3415        assert!(!chunk.code.is_empty());
3416        assert!(chunk.constants.contains(&Constant::Int(2)));
3417        assert!(chunk.constants.contains(&Constant::Int(3)));
3418    }
3419
3420    #[test]
3421    fn test_compile_function_call() {
3422        let chunk = compile_source("pipeline test(task) { log(42) }");
3423        let disasm = chunk.disassemble("test");
3424        assert!(disasm.contains("CALL"));
3425    }
3426
3427    #[test]
3428    fn test_compile_if_else() {
3429        let chunk =
3430            compile_source(r#"pipeline test(task) { if true { log("yes") } else { log("no") } }"#);
3431        let disasm = chunk.disassemble("test");
3432        assert!(disasm.contains("JUMP_IF_FALSE"));
3433        assert!(disasm.contains("JUMP"));
3434    }
3435
3436    #[test]
3437    fn test_compile_while() {
3438        let chunk = compile_source("pipeline test(task) { var i = 0\n while i < 5 { i = i + 1 } }");
3439        let disasm = chunk.disassemble("test");
3440        assert!(disasm.contains("JUMP_IF_FALSE"));
3441        assert!(disasm.contains("JUMP"));
3442    }
3443
3444    #[test]
3445    fn test_compile_closure() {
3446        let chunk = compile_source("pipeline test(task) { let f = { x -> x * 2 } }");
3447        assert!(!chunk.functions.is_empty());
3448        assert_eq!(chunk.functions[0].params, vec!["x"]);
3449    }
3450
3451    #[test]
3452    fn test_compile_list() {
3453        let chunk = compile_source("pipeline test(task) { let a = [1, 2, 3] }");
3454        let disasm = chunk.disassemble("test");
3455        assert!(disasm.contains("BUILD_LIST"));
3456    }
3457
3458    #[test]
3459    fn test_compile_dict() {
3460        let chunk = compile_source(r#"pipeline test(task) { let d = {name: "test"} }"#);
3461        let disasm = chunk.disassemble("test");
3462        assert!(disasm.contains("BUILD_DICT"));
3463    }
3464
3465    #[test]
3466    fn test_disassemble() {
3467        let chunk = compile_source("pipeline test(task) { log(2 + 3) }");
3468        let disasm = chunk.disassemble("test");
3469        assert!(disasm.contains("CONSTANT"));
3470        assert!(disasm.contains("ADD"));
3471        assert!(disasm.contains("CALL"));
3472    }
3473}