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