Skip to main content

harn_vm/compiler/
state.rs

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