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