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