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