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