Skip to main content

harn_vm/compiler/
state.rs

1use std::collections::BTreeMap;
2use std::rc::Rc;
3
4use harn_parser::{Node, SNode, ShapeField, TypeExpr, TypedParam};
5
6use crate::chunk::{Chunk, CompiledFunction, Constant, Op};
7use crate::value::VmValue;
8
9use super::error::CompileError;
10use super::yield_scan::body_contains_yield;
11use super::{peel_node, Compiler, FinallyEntry};
12
13impl Compiler {
14    pub fn new() -> Self {
15        Self {
16            chunk: Chunk::new(),
17            line: 1,
18            column: 1,
19            enum_names: std::collections::HashSet::new(),
20            interface_methods: std::collections::HashMap::new(),
21            loop_stack: Vec::new(),
22            handler_depth: 0,
23            finally_bodies: Vec::new(),
24            temp_counter: 0,
25            scope_depth: 0,
26            type_aliases: std::collections::HashMap::new(),
27            module_level: true,
28        }
29    }
30
31    /// Compiler instance for a nested function-like body (fn, closure,
32    /// tool, parallel arm, etc.). Differs from `new()` only in that
33    /// `module_level` starts false — `try*` is allowed inside.
34    pub(super) fn for_nested_body() -> Self {
35        let mut c = Self::new();
36        c.module_level = false;
37        c
38    }
39
40    /// Populate `type_aliases` from a program's top-level `type T = ...`
41    /// declarations so later lowerings can resolve alias names to their
42    /// canonical `TypeExpr`.
43    pub(super) fn collect_type_aliases(&mut self, program: &[SNode]) {
44        for sn in program {
45            if let Node::TypeDecl {
46                name,
47                type_expr,
48                type_params: _,
49            } = &sn.node
50            {
51                self.type_aliases.insert(name.clone(), type_expr.clone());
52            }
53        }
54    }
55
56    /// Expand a single layer of alias references. Returns the resolved
57    /// `TypeExpr` with all `Named(T)` nodes whose `T` is a known alias
58    /// replaced by the alias's body.
59    pub(super) fn expand_alias(&self, ty: &TypeExpr) -> TypeExpr {
60        match ty {
61            TypeExpr::Named(name) => {
62                if let Some(target) = self.type_aliases.get(name) {
63                    self.expand_alias(target)
64                } else {
65                    TypeExpr::Named(name.clone())
66                }
67            }
68            TypeExpr::Union(types) => {
69                TypeExpr::Union(types.iter().map(|t| self.expand_alias(t)).collect())
70            }
71            TypeExpr::Shape(fields) => TypeExpr::Shape(
72                fields
73                    .iter()
74                    .map(|field| ShapeField {
75                        name: field.name.clone(),
76                        type_expr: self.expand_alias(&field.type_expr),
77                        optional: field.optional,
78                    })
79                    .collect(),
80            ),
81            TypeExpr::List(inner) => TypeExpr::List(Box::new(self.expand_alias(inner))),
82            TypeExpr::Iter(inner) => TypeExpr::Iter(Box::new(self.expand_alias(inner))),
83            TypeExpr::DictType(k, v) => TypeExpr::DictType(
84                Box::new(self.expand_alias(k)),
85                Box::new(self.expand_alias(v)),
86            ),
87            TypeExpr::FnType {
88                params,
89                return_type,
90            } => TypeExpr::FnType {
91                params: params.iter().map(|p| self.expand_alias(p)).collect(),
92                return_type: Box::new(self.expand_alias(return_type)),
93            },
94            TypeExpr::Applied { name, args } => TypeExpr::Applied {
95                name: name.clone(),
96                args: args.iter().map(|a| self.expand_alias(a)).collect(),
97            },
98            TypeExpr::Never => TypeExpr::Never,
99            TypeExpr::LitString(s) => TypeExpr::LitString(s.clone()),
100            TypeExpr::LitInt(v) => TypeExpr::LitInt(*v),
101        }
102    }
103
104    /// Build the JSON-Schema VmValue for a named type alias, or `None` if
105    /// the name is unknown or the alias cannot be lowered to a schema.
106    pub(super) fn schema_value_for_alias(&self, name: &str) -> Option<VmValue> {
107        let ty = self.type_aliases.get(name)?;
108        let expanded = self.expand_alias(ty);
109        Self::type_expr_to_schema_value(&expanded)
110    }
111
112    /// Schema-guard builtins that accept a schema as their second argument.
113    /// When callers pass a type-alias identifier here, the compiler lowers
114    /// it to the alias's JSON-Schema dict constant.
115    pub(super) fn is_schema_guard(name: &str) -> bool {
116        matches!(
117            name,
118            "schema_is"
119                | "schema_expect"
120                | "schema_parse"
121                | "schema_check"
122                | "is_type"
123                | "json_validate"
124        )
125    }
126
127    /// Check whether a dict-literal key node matches the given keyword
128    /// (identifier or string literal form).
129    pub(super) fn entry_key_is(key: &SNode, keyword: &str) -> bool {
130        matches!(
131            &key.node,
132            Node::Identifier(name) | Node::StringLiteral(name) | Node::RawStringLiteral(name)
133                if name == keyword
134        )
135    }
136
137    /// Compile a program (list of top-level nodes) into a Chunk.
138    /// Finds the entry pipeline and compiles its body, including inherited bodies.
139    pub fn compile(mut self, program: &[SNode]) -> Result<Chunk, CompileError> {
140        // Pre-scan so we can recognize EnumName.Variant as enum construction
141        // even when the enum is declared inside a pipeline.
142        Self::collect_enum_names(program, &mut self.enum_names);
143        self.enum_names.insert("Result".to_string());
144        Self::collect_interface_methods(program, &mut self.interface_methods);
145        self.collect_type_aliases(program);
146
147        for sn in program {
148            match &sn.node {
149                Node::ImportDecl { .. } | Node::SelectiveImport { .. } => {
150                    self.compile_node(sn)?;
151                }
152                _ => {}
153            }
154        }
155        let main = program
156            .iter()
157            .find(|sn| matches!(peel_node(sn), Node::Pipeline { name, .. } if name == "default"))
158            .or_else(|| {
159                program
160                    .iter()
161                    .find(|sn| matches!(peel_node(sn), Node::Pipeline { .. }))
162            });
163
164        if let Some(sn) = main {
165            self.compile_top_level_declarations(program)?;
166            if let Node::Pipeline { body, extends, .. } = peel_node(sn) {
167                if let Some(parent_name) = extends {
168                    self.compile_parent_pipeline(program, parent_name)?;
169                }
170                let saved = std::mem::replace(&mut self.module_level, false);
171                self.compile_block(body)?;
172                self.module_level = saved;
173            }
174        } else {
175            // Script mode: no pipeline found, treat top-level as implicit entry.
176            let top_level: Vec<&SNode> = program
177                .iter()
178                .filter(|sn| {
179                    !matches!(
180                        &sn.node,
181                        Node::ImportDecl { .. } | Node::SelectiveImport { .. }
182                    )
183                })
184                .collect();
185            for sn in &top_level {
186                self.compile_node(sn)?;
187                if Self::produces_value(&sn.node) {
188                    self.chunk.emit(Op::Pop, self.line);
189                }
190            }
191        }
192
193        for fb in self.all_pending_finallys() {
194            self.compile_finally_inline(&fb)?;
195        }
196        self.chunk.emit(Op::Nil, self.line);
197        self.chunk.emit(Op::Return, self.line);
198        Ok(self.chunk)
199    }
200
201    /// Compile a specific named pipeline (for test runners).
202    pub fn compile_named(
203        mut self,
204        program: &[SNode],
205        pipeline_name: &str,
206    ) -> Result<Chunk, CompileError> {
207        Self::collect_enum_names(program, &mut self.enum_names);
208        Self::collect_interface_methods(program, &mut self.interface_methods);
209        self.collect_type_aliases(program);
210
211        for sn in program {
212            if matches!(
213                &sn.node,
214                Node::ImportDecl { .. } | Node::SelectiveImport { .. }
215            ) {
216                self.compile_node(sn)?;
217            }
218        }
219        let target = program.iter().find(
220            |sn| matches!(peel_node(sn), Node::Pipeline { name, .. } if name == pipeline_name),
221        );
222
223        if let Some(sn) = target {
224            self.compile_top_level_declarations(program)?;
225            if let Node::Pipeline { body, extends, .. } = peel_node(sn) {
226                if let Some(parent_name) = extends {
227                    self.compile_parent_pipeline(program, parent_name)?;
228                }
229                let saved = std::mem::replace(&mut self.module_level, false);
230                self.compile_block(body)?;
231                self.module_level = saved;
232            }
233        }
234
235        for fb in self.all_pending_finallys() {
236            self.compile_finally_inline(&fb)?;
237        }
238        self.chunk.emit(Op::Nil, self.line);
239        self.chunk.emit(Op::Return, self.line);
240        Ok(self.chunk)
241    }
242
243    /// Recursively compile parent pipeline bodies (for extends).
244    pub(super) fn compile_parent_pipeline(
245        &mut self,
246        program: &[SNode],
247        parent_name: &str,
248    ) -> Result<(), CompileError> {
249        let parent = program
250            .iter()
251            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == parent_name));
252        if let Some(sn) = parent {
253            if let Node::Pipeline { body, extends, .. } = &sn.node {
254                if let Some(grandparent) = extends {
255                    self.compile_parent_pipeline(program, grandparent)?;
256                }
257                for stmt in body {
258                    self.compile_node(stmt)?;
259                    if Self::produces_value(&stmt.node) {
260                        self.chunk.emit(Op::Pop, self.line);
261                    }
262                }
263            }
264        }
265        Ok(())
266    }
267
268    /// Emit bytecode preamble for default parameter values.
269    /// For each param with a default at index i, emits:
270    ///   GetArgc; PushInt (i+1); GreaterEqual; JumpIfTrue <skip>;
271    ///   [compile default expr]; DefLet param_name; <skip>:
272    pub(super) fn emit_default_preamble(
273        &mut self,
274        params: &[TypedParam],
275    ) -> Result<(), CompileError> {
276        for (i, param) in params.iter().enumerate() {
277            if let Some(default_expr) = &param.default_value {
278                self.chunk.emit(Op::GetArgc, self.line);
279                let threshold_idx = self.chunk.add_constant(Constant::Int((i + 1) as i64));
280                self.chunk.emit_u16(Op::Constant, threshold_idx, self.line);
281                self.chunk.emit(Op::GreaterEqual, self.line);
282                let skip_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
283                // JumpIfTrue doesn't pop its boolean operand.
284                self.chunk.emit(Op::Pop, self.line);
285                self.compile_node(default_expr)?;
286                let name_idx = self
287                    .chunk
288                    .add_constant(Constant::String(param.name.clone()));
289                self.chunk.emit_u16(Op::DefLet, name_idx, self.line);
290                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
291                self.chunk.patch_jump(skip_jump);
292                self.chunk.emit(Op::Pop, self.line);
293                self.chunk.patch_jump(end_jump);
294            }
295        }
296        Ok(())
297    }
298
299    /// Emit runtime type checks for parameters with type annotations.
300    /// Interface types keep their dedicated runtime guard; all other supported
301    /// runtime-checkable types compile to a schema literal and call
302    /// `__assert_schema(value, param_name, schema)`.
303    pub(super) fn emit_type_checks(&mut self, params: &[TypedParam]) {
304        for param in params {
305            if let Some(type_expr) = &param.type_expr {
306                if let harn_parser::TypeExpr::Named(name) = type_expr {
307                    if let Some(methods) = self.interface_methods.get(name) {
308                        let fn_idx = self
309                            .chunk
310                            .add_constant(Constant::String("__assert_interface".into()));
311                        self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
312                        let var_idx = self
313                            .chunk
314                            .add_constant(Constant::String(param.name.clone()));
315                        self.chunk.emit_u16(Op::GetVar, var_idx, self.line);
316                        let name_idx = self
317                            .chunk
318                            .add_constant(Constant::String(param.name.clone()));
319                        self.chunk.emit_u16(Op::Constant, name_idx, self.line);
320                        let iface_idx = self.chunk.add_constant(Constant::String(name.clone()));
321                        self.chunk.emit_u16(Op::Constant, iface_idx, self.line);
322                        let methods_str = methods.join(",");
323                        let methods_idx = self.chunk.add_constant(Constant::String(methods_str));
324                        self.chunk.emit_u16(Op::Constant, methods_idx, self.line);
325                        self.chunk.emit_u8(Op::Call, 4, self.line);
326                        self.chunk.emit(Op::Pop, self.line);
327                        continue;
328                    }
329                }
330
331                if let Some(schema) = Self::type_expr_to_schema_value(type_expr) {
332                    let fn_idx = self
333                        .chunk
334                        .add_constant(Constant::String("__assert_schema".into()));
335                    self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
336                    let var_idx = self
337                        .chunk
338                        .add_constant(Constant::String(param.name.clone()));
339                    self.chunk.emit_u16(Op::GetVar, var_idx, self.line);
340                    let name_idx = self
341                        .chunk
342                        .add_constant(Constant::String(param.name.clone()));
343                    self.chunk.emit_u16(Op::Constant, name_idx, self.line);
344                    self.emit_vm_value_literal(&schema);
345                    self.chunk.emit_u8(Op::Call, 3, self.line);
346                    self.chunk.emit(Op::Pop, self.line);
347                }
348            }
349        }
350    }
351
352    pub(crate) fn type_expr_to_schema_value(type_expr: &harn_parser::TypeExpr) -> Option<VmValue> {
353        match type_expr {
354            harn_parser::TypeExpr::Named(name) => match name.as_str() {
355                "int" | "float" | "string" | "bool" | "list" | "dict" | "set" | "nil"
356                | "closure" => Some(VmValue::Dict(Rc::new(BTreeMap::from([(
357                    "type".to_string(),
358                    VmValue::String(Rc::from(name.as_str())),
359                )])))),
360                _ => None,
361            },
362            harn_parser::TypeExpr::Shape(fields) => {
363                let mut properties = BTreeMap::new();
364                let mut required = Vec::new();
365                for field in fields {
366                    let field_schema = Self::type_expr_to_schema_value(&field.type_expr)?;
367                    properties.insert(field.name.clone(), field_schema);
368                    if !field.optional {
369                        required.push(VmValue::String(Rc::from(field.name.as_str())));
370                    }
371                }
372                let mut out = BTreeMap::new();
373                out.insert("type".to_string(), VmValue::String(Rc::from("dict")));
374                out.insert("properties".to_string(), VmValue::Dict(Rc::new(properties)));
375                if !required.is_empty() {
376                    out.insert("required".to_string(), VmValue::List(Rc::new(required)));
377                }
378                Some(VmValue::Dict(Rc::new(out)))
379            }
380            harn_parser::TypeExpr::List(inner) => {
381                let mut out = BTreeMap::new();
382                out.insert("type".to_string(), VmValue::String(Rc::from("list")));
383                if let Some(item_schema) = Self::type_expr_to_schema_value(inner) {
384                    out.insert("items".to_string(), item_schema);
385                }
386                Some(VmValue::Dict(Rc::new(out)))
387            }
388            harn_parser::TypeExpr::DictType(key, value) => {
389                let mut out = BTreeMap::new();
390                out.insert("type".to_string(), VmValue::String(Rc::from("dict")));
391                if matches!(key.as_ref(), harn_parser::TypeExpr::Named(name) if name == "string") {
392                    if let Some(value_schema) = Self::type_expr_to_schema_value(value) {
393                        out.insert("additional_properties".to_string(), value_schema);
394                    }
395                }
396                Some(VmValue::Dict(Rc::new(out)))
397            }
398            harn_parser::TypeExpr::Union(members) => {
399                // Special-case unions of literals: emit as `enum: [...]`
400                // so the schema round-trips as canonical JSON Schema and
401                // is ACP-/OpenAPI-compatible. Mixed unions fall back to
402                // the `union:` key that validators recognize.
403                if !members.is_empty()
404                    && members
405                        .iter()
406                        .all(|m| matches!(m, harn_parser::TypeExpr::LitString(_)))
407                {
408                    let values = members
409                        .iter()
410                        .map(|m| match m {
411                            harn_parser::TypeExpr::LitString(s) => {
412                                VmValue::String(Rc::from(s.as_str()))
413                            }
414                            _ => unreachable!(),
415                        })
416                        .collect::<Vec<_>>();
417                    return Some(VmValue::Dict(Rc::new(BTreeMap::from([
418                        ("type".to_string(), VmValue::String(Rc::from("string"))),
419                        ("enum".to_string(), VmValue::List(Rc::new(values))),
420                    ]))));
421                }
422                if !members.is_empty()
423                    && members
424                        .iter()
425                        .all(|m| matches!(m, harn_parser::TypeExpr::LitInt(_)))
426                {
427                    let values = members
428                        .iter()
429                        .map(|m| match m {
430                            harn_parser::TypeExpr::LitInt(v) => VmValue::Int(*v),
431                            _ => unreachable!(),
432                        })
433                        .collect::<Vec<_>>();
434                    return Some(VmValue::Dict(Rc::new(BTreeMap::from([
435                        ("type".to_string(), VmValue::String(Rc::from("int"))),
436                        ("enum".to_string(), VmValue::List(Rc::new(values))),
437                    ]))));
438                }
439                let branches = members
440                    .iter()
441                    .filter_map(Self::type_expr_to_schema_value)
442                    .collect::<Vec<_>>();
443                if branches.is_empty() {
444                    None
445                } else {
446                    Some(VmValue::Dict(Rc::new(BTreeMap::from([(
447                        "union".to_string(),
448                        VmValue::List(Rc::new(branches)),
449                    )]))))
450                }
451            }
452            harn_parser::TypeExpr::FnType { .. } => {
453                Some(VmValue::Dict(Rc::new(BTreeMap::from([(
454                    "type".to_string(),
455                    VmValue::String(Rc::from("closure")),
456                )]))))
457            }
458            harn_parser::TypeExpr::Applied { .. } => None,
459            harn_parser::TypeExpr::Iter(_) => None,
460            harn_parser::TypeExpr::Never => None,
461            harn_parser::TypeExpr::LitString(s) => Some(VmValue::Dict(Rc::new(BTreeMap::from([
462                ("type".to_string(), VmValue::String(Rc::from("string"))),
463                ("const".to_string(), VmValue::String(Rc::from(s.as_str()))),
464            ])))),
465            harn_parser::TypeExpr::LitInt(v) => Some(VmValue::Dict(Rc::new(BTreeMap::from([
466                ("type".to_string(), VmValue::String(Rc::from("int"))),
467                ("const".to_string(), VmValue::Int(*v)),
468            ])))),
469        }
470    }
471
472    pub(super) fn emit_vm_value_literal(&mut self, value: &VmValue) {
473        match value {
474            VmValue::String(text) => {
475                let idx = self.chunk.add_constant(Constant::String(text.to_string()));
476                self.chunk.emit_u16(Op::Constant, idx, self.line);
477            }
478            VmValue::Int(number) => {
479                let idx = self.chunk.add_constant(Constant::Int(*number));
480                self.chunk.emit_u16(Op::Constant, idx, self.line);
481            }
482            VmValue::Float(number) => {
483                let idx = self.chunk.add_constant(Constant::Float(*number));
484                self.chunk.emit_u16(Op::Constant, idx, self.line);
485            }
486            VmValue::Bool(value) => {
487                let idx = self.chunk.add_constant(Constant::Bool(*value));
488                self.chunk.emit_u16(Op::Constant, idx, self.line);
489            }
490            VmValue::Nil => self.chunk.emit(Op::Nil, self.line),
491            VmValue::List(items) => {
492                for item in items.iter() {
493                    self.emit_vm_value_literal(item);
494                }
495                self.chunk
496                    .emit_u16(Op::BuildList, items.len() as u16, self.line);
497            }
498            VmValue::Dict(entries) => {
499                for (key, item) in entries.iter() {
500                    let key_idx = self.chunk.add_constant(Constant::String(key.clone()));
501                    self.chunk.emit_u16(Op::Constant, key_idx, self.line);
502                    self.emit_vm_value_literal(item);
503                }
504                self.chunk
505                    .emit_u16(Op::BuildDict, entries.len() as u16, self.line);
506            }
507            _ => {}
508        }
509    }
510
511    /// Emit the extra u16 type name index after a TryCatchSetup jump.
512    pub(super) fn emit_type_name_extra(&mut self, type_name_idx: u16) {
513        let hi = (type_name_idx >> 8) as u8;
514        let lo = type_name_idx as u8;
515        self.chunk.code.push(hi);
516        self.chunk.code.push(lo);
517        self.chunk.lines.push(self.line);
518        self.chunk.columns.push(self.column);
519        self.chunk.lines.push(self.line);
520        self.chunk.columns.push(self.column);
521    }
522
523    /// Compile a try/catch body block (produces a value on the stack).
524    pub(super) fn compile_try_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
525        if body.is_empty() {
526            self.chunk.emit(Op::Nil, self.line);
527        } else {
528            self.compile_scoped_block(body)?;
529        }
530        Ok(())
531    }
532
533    /// Compile catch error binding (error value is on stack from handler).
534    pub(super) fn compile_catch_binding(
535        &mut self,
536        error_var: &Option<String>,
537    ) -> Result<(), CompileError> {
538        if let Some(var_name) = error_var {
539            let idx = self.chunk.add_constant(Constant::String(var_name.clone()));
540            self.chunk.emit_u16(Op::DefLet, idx, self.line);
541        } else {
542            self.chunk.emit(Op::Pop, self.line);
543        }
544        Ok(())
545    }
546
547    /// Compile finally body inline, discarding its result value.
548    /// `compile_scoped_block` always leaves exactly one value on the stack
549    /// (Nil for non-value tail statements), so the trailing Pop is
550    /// unconditional — otherwise a finally ending in e.g. `x = x + 1`
551    /// would leave a stray Nil that corrupts the surrounding expression
552    /// when the enclosing try/finally is used in expression position.
553    pub(super) fn compile_finally_inline(
554        &mut self,
555        finally_body: &[SNode],
556    ) -> Result<(), CompileError> {
557        if !finally_body.is_empty() {
558            self.compile_scoped_block(finally_body)?;
559            self.chunk.emit(Op::Pop, self.line);
560        }
561        Ok(())
562    }
563
564    /// Collect pending finally bodies from the top of the stack down to
565    /// (but not including) the innermost `CatchBarrier`. Used by `throw`
566    /// lowering: throws caught locally don't unwind past the catch, so
567    /// finallys behind the barrier aren't on the throw's exit path.
568    pub(super) fn pending_finallys_until_barrier(&self) -> Vec<Vec<SNode>> {
569        let mut out = Vec::new();
570        for entry in self.finally_bodies.iter().rev() {
571            match entry {
572                FinallyEntry::CatchBarrier => break,
573                FinallyEntry::Finally(body) => out.push(body.clone()),
574            }
575        }
576        out
577    }
578
579    /// Collect every pending finally body from the top of the stack down
580    /// to `floor` (an index produced by `finally_bodies.len()` at some
581    /// earlier point), skipping `CatchBarrier` markers. Used by `return`,
582    /// `break`, and `continue` lowering — they transfer control past local
583    /// handlers, so every `Finally` up to their target must run.
584    pub(super) fn pending_finallys_down_to(&self, floor: usize) -> Vec<Vec<SNode>> {
585        let mut out = Vec::new();
586        for entry in self.finally_bodies[floor..].iter().rev() {
587            if let FinallyEntry::Finally(body) = entry {
588                out.push(body.clone());
589            }
590        }
591        out
592    }
593
594    /// All pending finally bodies (entire stack), skipping barriers.
595    pub(super) fn all_pending_finallys(&self) -> Vec<Vec<SNode>> {
596        self.pending_finallys_down_to(0)
597    }
598
599    /// True if there are any pending finally bodies (not just barriers).
600    pub(super) fn has_pending_finally(&self) -> bool {
601        self.finally_bodies
602            .iter()
603            .any(|e| matches!(e, FinallyEntry::Finally(_)))
604    }
605
606    /// Save a thrown value to a temp and rethrow without running finally.
607    ///
608    /// Historically this helper also invoked `compile_finally_inline` on the
609    /// thrown path, but that produced observable double-runs: the
610    /// `Node::ThrowStmt` lowering (below) already iterates `finally_bodies`
611    /// and runs each pending finally inline *before* emitting `Op::Throw`, so
612    /// a second run here fired the same side effects twice. Finally now runs
613    /// exactly once — via the throw-emit path during unwinding.
614    pub(super) fn compile_plain_rethrow(&mut self) -> Result<(), CompileError> {
615        self.temp_counter += 1;
616        let temp_name = format!("__finally_err_{}__", self.temp_counter);
617        let err_idx = self.chunk.add_constant(Constant::String(temp_name.clone()));
618        self.chunk.emit_u16(Op::DefVar, err_idx, self.line);
619        let get_idx = self.chunk.add_constant(Constant::String(temp_name));
620        self.chunk.emit_u16(Op::GetVar, get_idx, self.line);
621        self.chunk.emit(Op::Throw, self.line);
622        Ok(())
623    }
624
625    pub(super) fn begin_scope(&mut self) {
626        self.chunk.emit(Op::PushScope, self.line);
627        self.scope_depth += 1;
628    }
629
630    pub(super) fn end_scope(&mut self) {
631        if self.scope_depth > 0 {
632            self.chunk.emit(Op::PopScope, self.line);
633            self.scope_depth -= 1;
634        }
635    }
636
637    pub(super) fn unwind_scopes_to(&mut self, target_depth: usize) {
638        while self.scope_depth > target_depth {
639            self.chunk.emit(Op::PopScope, self.line);
640            self.scope_depth -= 1;
641        }
642    }
643
644    pub(super) fn compile_scoped_block(&mut self, stmts: &[SNode]) -> Result<(), CompileError> {
645        self.begin_scope();
646        if stmts.is_empty() {
647            self.chunk.emit(Op::Nil, self.line);
648        } else {
649            self.compile_block(stmts)?;
650        }
651        self.end_scope();
652        Ok(())
653    }
654
655    pub(super) fn compile_scoped_statements(
656        &mut self,
657        stmts: &[SNode],
658    ) -> Result<(), CompileError> {
659        self.begin_scope();
660        for sn in stmts {
661            self.compile_node(sn)?;
662            if Self::produces_value(&sn.node) {
663                self.chunk.emit(Op::Pop, self.line);
664            }
665        }
666        self.end_scope();
667        Ok(())
668    }
669
670    pub(super) fn compile_block(&mut self, stmts: &[SNode]) -> Result<(), CompileError> {
671        for (i, snode) in stmts.iter().enumerate() {
672            self.compile_node(snode)?;
673            let is_last = i == stmts.len() - 1;
674            if is_last {
675                // Ensure the block always leaves exactly one value on the stack.
676                if !Self::produces_value(&snode.node) {
677                    self.chunk.emit(Op::Nil, self.line);
678                }
679            } else if Self::produces_value(&snode.node) {
680                self.chunk.emit(Op::Pop, self.line);
681            }
682        }
683        Ok(())
684    }
685
686    /// Compile a match arm body, ensuring it always pushes exactly one value.
687    pub(super) fn compile_match_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
688        self.begin_scope();
689        if body.is_empty() {
690            self.chunk.emit(Op::Nil, self.line);
691        } else {
692            self.compile_block(body)?;
693            if !Self::produces_value(&body.last().unwrap().node) {
694                self.chunk.emit(Op::Nil, self.line);
695            }
696        }
697        self.end_scope();
698        Ok(())
699    }
700
701    /// Emit the binary op instruction for a compound assignment operator.
702    pub(super) fn emit_compound_op(&mut self, op: &str) -> Result<(), CompileError> {
703        match op {
704            "+" => self.chunk.emit(Op::Add, self.line),
705            "-" => self.chunk.emit(Op::Sub, self.line),
706            "*" => self.chunk.emit(Op::Mul, self.line),
707            "/" => self.chunk.emit(Op::Div, self.line),
708            "%" => self.chunk.emit(Op::Mod, self.line),
709            _ => {
710                return Err(CompileError {
711                    message: format!("Unknown compound operator: {op}"),
712                    line: self.line,
713                })
714            }
715        }
716        Ok(())
717    }
718
719    /// Extract the root variable name from a (possibly nested) access expression.
720    pub(super) fn root_var_name(&self, node: &SNode) -> Option<String> {
721        match &node.node {
722            Node::Identifier(name) => Some(name.clone()),
723            Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
724                self.root_var_name(object)
725            }
726            Node::SubscriptAccess { object, .. } => self.root_var_name(object),
727            _ => None,
728        }
729    }
730
731    pub(super) fn compile_top_level_declarations(
732        &mut self,
733        program: &[SNode],
734    ) -> Result<(), CompileError> {
735        // Phase 1: evaluate module-level `let` / `var` bindings first, in
736        // source order. This ensures function closures compiled in phase 2
737        // capture these names in their env snapshot via `Op::Closure` —
738        // fixing the "Undefined variable: FOO" surprise where a top-level
739        // `let FOO = "..."` was silently dropped because it wasn't in this
740        // match list. Keep in step with the import-time init path in
741        // `crates/harn-vm/src/vm/imports.rs` (`module_state` construction).
742        for sn in program {
743            if matches!(&sn.node, Node::LetBinding { .. } | Node::VarBinding { .. }) {
744                self.compile_node(sn)?;
745            }
746        }
747        // Phase 2: compile type and function declarations. Function closures
748        // created here capture the current env which now includes the
749        // module-level bindings from phase 1. Attributed declarations are
750        // compiled here too — the AttributedDecl arm in compile_node
751        // dispatches to the inner declaration's compile path.
752        for sn in program {
753            let inner_kind = match &sn.node {
754                Node::AttributedDecl { inner, .. } => &inner.node,
755                other => other,
756            };
757            if matches!(
758                inner_kind,
759                Node::FnDecl { .. }
760                    | Node::ToolDecl { .. }
761                    | Node::SkillDecl { .. }
762                    | Node::ImplBlock { .. }
763                    | Node::StructDecl { .. }
764                    | Node::EnumDecl { .. }
765                    | Node::InterfaceDecl { .. }
766                    | Node::TypeDecl { .. }
767            ) {
768                self.compile_node(sn)?;
769            }
770        }
771        Ok(())
772    }
773
774    /// Recursively collect all enum type names from the AST.
775    pub(super) fn collect_enum_names(
776        nodes: &[SNode],
777        names: &mut std::collections::HashSet<String>,
778    ) {
779        for sn in nodes {
780            match &sn.node {
781                Node::EnumDecl { name, .. } => {
782                    names.insert(name.clone());
783                }
784                Node::Pipeline { body, .. } => {
785                    Self::collect_enum_names(body, names);
786                }
787                Node::FnDecl { body, .. } | Node::ToolDecl { body, .. } => {
788                    Self::collect_enum_names(body, names);
789                }
790                Node::SkillDecl { fields, .. } => {
791                    for (_k, v) in fields {
792                        Self::collect_enum_names(std::slice::from_ref(v), names);
793                    }
794                }
795                Node::Block(stmts) => {
796                    Self::collect_enum_names(stmts, names);
797                }
798                Node::AttributedDecl { inner, .. } => {
799                    Self::collect_enum_names(std::slice::from_ref(inner), names);
800                }
801                _ => {}
802            }
803        }
804    }
805
806    pub(super) fn collect_interface_methods(
807        nodes: &[SNode],
808        interfaces: &mut std::collections::HashMap<String, Vec<String>>,
809    ) {
810        for sn in nodes {
811            match &sn.node {
812                Node::InterfaceDecl { name, methods, .. } => {
813                    let method_names: Vec<String> =
814                        methods.iter().map(|m| m.name.clone()).collect();
815                    interfaces.insert(name.clone(), method_names);
816                }
817                Node::Pipeline { body, .. }
818                | Node::FnDecl { body, .. }
819                | Node::ToolDecl { body, .. } => {
820                    Self::collect_interface_methods(body, interfaces);
821                }
822                Node::SkillDecl { fields, .. } => {
823                    for (_k, v) in fields {
824                        Self::collect_interface_methods(std::slice::from_ref(v), interfaces);
825                    }
826                }
827                Node::Block(stmts) => {
828                    Self::collect_interface_methods(stmts, interfaces);
829                }
830                Node::AttributedDecl { inner, .. } => {
831                    Self::collect_interface_methods(std::slice::from_ref(inner), interfaces);
832                }
833                _ => {}
834            }
835        }
836    }
837
838    /// Compile a function body into a CompiledFunction (for import support).
839    ///
840    /// This path is used when a module is imported and its top-level `fn`
841    /// declarations are loaded into the importer's environment. It MUST emit
842    /// the same function preamble as the in-file `Node::FnDecl` path, or
843    /// imported functions will behave differently from locally-defined ones —
844    /// in particular, default parameter values would never be set and typed
845    /// parameters would not be runtime-checked.
846    ///
847    /// `source_file`, when provided, tags the resulting chunk so runtime
848    /// errors can attribute frames to the imported file rather than the
849    /// entry-point pipeline.
850    pub fn compile_fn_body(
851        &mut self,
852        params: &[TypedParam],
853        body: &[SNode],
854        source_file: Option<String>,
855    ) -> Result<CompiledFunction, CompileError> {
856        let mut fn_compiler = Compiler::for_nested_body();
857        fn_compiler.enum_names = self.enum_names.clone();
858        fn_compiler.emit_default_preamble(params)?;
859        fn_compiler.emit_type_checks(params);
860        let is_gen = body_contains_yield(body);
861        fn_compiler.compile_block(body)?;
862        fn_compiler.chunk.emit(Op::Nil, 0);
863        fn_compiler.chunk.emit(Op::Return, 0);
864        fn_compiler.chunk.source_file = source_file;
865        Ok(CompiledFunction {
866            name: String::new(),
867            params: TypedParam::names(params),
868            default_start: TypedParam::default_start(params),
869            chunk: fn_compiler.chunk,
870            is_generator: is_gen,
871            has_rest_param: false,
872        })
873    }
874
875    /// Check if a node produces a value on the stack that needs to be popped.
876    pub(super) fn produces_value(node: &Node) -> bool {
877        match node {
878            Node::LetBinding { .. }
879            | Node::VarBinding { .. }
880            | Node::Assignment { .. }
881            | Node::ReturnStmt { .. }
882            | Node::FnDecl { .. }
883            | Node::ToolDecl { .. }
884            | Node::SkillDecl { .. }
885            | Node::ImplBlock { .. }
886            | Node::StructDecl { .. }
887            | Node::EnumDecl { .. }
888            | Node::InterfaceDecl { .. }
889            | Node::TypeDecl { .. }
890            | Node::ThrowStmt { .. }
891            | Node::BreakStmt
892            | Node::ContinueStmt
893            | Node::RequireStmt { .. }
894            | Node::DeferStmt { .. } => false,
895            Node::TryCatch { .. }
896            | Node::TryExpr { .. }
897            | Node::Retry { .. }
898            | Node::GuardStmt { .. }
899            | Node::DeadlineBlock { .. }
900            | Node::MutexBlock { .. }
901            | Node::Spread(_) => true,
902            _ => true,
903        }
904    }
905}
906
907impl Default for Compiler {
908    fn default() -> Self {
909        Self::new()
910    }
911}