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