Skip to main content

harn_vm/compiler/
state.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
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
13#[cfg(test)]
14thread_local! {
15    /// Test-only override for the value-discarding classification used by
16    /// [`Compiler::compile_discarded_stmt`]. Setting it forces a
17    /// `produces_value` answer regardless of the node, letting tests
18    /// deliberately miswire the classification and prove the #2622 balance
19    /// assertion fires (see
20    /// `compiler::tests::miswired_produces_value_trips_balance_assertion`).
21    pub(super) static FORCE_DISCARDED_PRODUCES_VALUE: std::cell::Cell<Option<bool>> =
22        const { std::cell::Cell::new(None) };
23}
24
25impl Compiler {
26    pub fn new() -> Self {
27        Self::with_options(CompilerOptions::from_env())
28    }
29
30    pub fn with_options(options: CompilerOptions) -> Self {
31        Self {
32            options,
33            chunk: Chunk::new(),
34            line: 1,
35            column: 1,
36            enum_names: std::collections::HashSet::new(),
37            struct_layouts: std::collections::HashMap::new(),
38            interface_methods: std::collections::HashMap::new(),
39            loop_stack: Vec::new(),
40            handler_depth: 0,
41            finally_bodies: Vec::new(),
42            temp_counter: 0,
43            scope_depth: 0,
44            type_aliases: std::collections::HashMap::new(),
45            type_scopes: vec![std::collections::HashMap::new()],
46            monomorphic_bindings: std::collections::HashSet::new(),
47            string_constants: std::collections::HashMap::new(),
48            local_scopes: vec![std::collections::HashMap::new()],
49            module_level: true,
50        }
51    }
52
53    /// Compiler instance for a nested function-like body (fn, closure,
54    /// tool, parallel arm, etc.). Differs from `new()` only in that
55    /// `module_level` starts false — `try*` is allowed inside.
56    pub(super) fn for_nested_body(options: CompilerOptions) -> Self {
57        let mut c = Self::with_options(options);
58        c.module_level = false;
59        c
60    }
61
62    pub(super) fn nested_body(&self) -> Self {
63        Self::for_nested_body(self.options)
64    }
65
66    pub(super) fn nominal_type_names(&self) -> Vec<String> {
67        let mut names: Vec<String> = self
68            .struct_layouts
69            .keys()
70            .chain(self.enum_names.iter())
71            .cloned()
72            .collect();
73        names.sort();
74        names.dedup();
75        names
76    }
77
78    pub(super) fn string_constant(&mut self, value: &str) -> u16 {
79        if let Some(idx) = self.string_constants.get(value) {
80            return *idx;
81        }
82        let owned = value.to_string();
83        let idx = self.chunk.add_constant(Constant::String(owned.clone()));
84        self.string_constants.insert(owned, idx);
85        idx
86    }
87
88    pub(super) fn owned_string_constant(&mut self, value: String) -> u16 {
89        if let Some(idx) = self.string_constants.get(value.as_str()) {
90            return *idx;
91        }
92        let idx = self.chunk.add_constant(Constant::String(value.clone()));
93        self.string_constants.insert(value, idx);
94        idx
95    }
96
97    /// Populate `type_aliases` from a program's top-level `type T = ...`
98    /// declarations so later lowerings can resolve alias names to their
99    /// canonical `TypeExpr`.
100    pub(super) fn collect_type_aliases(&mut self, program: &[SNode]) {
101        for sn in program {
102            if let Node::TypeDecl {
103                name,
104                type_expr,
105                type_params: _,
106            } = &sn.node
107            {
108                self.type_aliases.insert(name.clone(), type_expr.clone());
109            }
110        }
111    }
112
113    /// Expand a single layer of alias references. Returns the resolved
114    /// `TypeExpr` with all `Named(T)` nodes whose `T` is a known alias
115    /// replaced by the alias's body.
116    pub(super) fn expand_alias(&self, ty: &TypeExpr) -> TypeExpr {
117        match ty {
118            TypeExpr::Named(name) => {
119                if let Some(target) = self.type_aliases.get(name) {
120                    self.expand_alias(target)
121                } else {
122                    TypeExpr::Named(name.clone())
123                }
124            }
125            TypeExpr::Union(types) => {
126                TypeExpr::Union(types.iter().map(|t| self.expand_alias(t)).collect())
127            }
128            TypeExpr::Intersection(types) => {
129                TypeExpr::Intersection(types.iter().map(|t| self.expand_alias(t)).collect())
130            }
131            TypeExpr::Shape(fields) => TypeExpr::Shape(
132                fields
133                    .iter()
134                    .map(|field| ShapeField {
135                        name: field.name.clone(),
136                        type_expr: self.expand_alias(&field.type_expr),
137                        optional: field.optional,
138                    })
139                    .collect(),
140            ),
141            TypeExpr::OpenShape { fields, rests } => TypeExpr::OpenShape {
142                fields: fields
143                    .iter()
144                    .map(|field| ShapeField {
145                        name: field.name.clone(),
146                        type_expr: self.expand_alias(&field.type_expr),
147                        optional: field.optional,
148                    })
149                    .collect(),
150                rests: rests.iter().map(|r| self.expand_alias(r)).collect(),
151            },
152            TypeExpr::List(inner) => TypeExpr::List(Box::new(self.expand_alias(inner))),
153            TypeExpr::Iter(inner) => TypeExpr::Iter(Box::new(self.expand_alias(inner))),
154            TypeExpr::Generator(inner) => TypeExpr::Generator(Box::new(self.expand_alias(inner))),
155            TypeExpr::Stream(inner) => TypeExpr::Stream(Box::new(self.expand_alias(inner))),
156            TypeExpr::DictType(k, v) => TypeExpr::DictType(
157                Box::new(self.expand_alias(k)),
158                Box::new(self.expand_alias(v)),
159            ),
160            TypeExpr::FnType {
161                params,
162                return_type,
163            } => TypeExpr::FnType {
164                params: params.iter().map(|p| self.expand_alias(p)).collect(),
165                return_type: Box::new(self.expand_alias(return_type)),
166            },
167            TypeExpr::Applied { name, args } => TypeExpr::Applied {
168                name: name.clone(),
169                args: args.iter().map(|a| self.expand_alias(a)).collect(),
170            },
171            TypeExpr::Never => TypeExpr::Never,
172            TypeExpr::LitString(s) => TypeExpr::LitString(s.clone()),
173            TypeExpr::LitInt(v) => TypeExpr::LitInt(*v),
174            TypeExpr::Owned(inner) => TypeExpr::Owned(Box::new(self.expand_alias(inner))),
175        }
176    }
177
178    /// Build the JSON-Schema VmValue for a named type alias, or `None` if
179    /// the name is unknown or the alias cannot be lowered to a schema.
180    pub(super) fn schema_value_for_alias(&self, name: &str) -> Option<VmValue> {
181        let ty = self.type_aliases.get(name)?;
182        let expanded = self.expand_alias(ty);
183        Self::type_expr_to_schema_value(&expanded)
184    }
185
186    /// Schema-guard builtins that accept a schema as their second argument.
187    /// When callers pass a type-alias identifier here, the compiler lowers
188    /// it to the alias's JSON-Schema dict constant.
189    pub(super) fn is_schema_guard(name: &str) -> bool {
190        matches!(
191            name,
192            "schema_is"
193                | "schema_expect"
194                | "schema_parse"
195                | "schema_check"
196                | "is_type"
197                | "json_validate"
198        )
199    }
200
201    /// Check whether a dict-literal key node matches the given keyword
202    /// (identifier or string literal form).
203    pub(super) fn entry_key_is(key: &SNode, keyword: &str) -> bool {
204        matches!(
205            &key.node,
206            Node::Identifier(name) | Node::StringLiteral(name) | Node::RawStringLiteral(name)
207                if name == keyword
208        )
209    }
210
211    /// Compile a program (list of top-level nodes) into a Chunk.
212    /// Finds the entry pipeline and compiles its body, including inherited bodies.
213    pub fn compile(mut self, program: &[SNode]) -> Result<Chunk, CompileError> {
214        // Pre-scan so we can recognize EnumName.Variant as enum construction
215        // even when the enum is declared inside a pipeline.
216        Self::collect_enum_names(program, &mut self.enum_names);
217        self.enum_names.insert("Result".to_string());
218        Self::collect_struct_layouts(program, &mut self.struct_layouts);
219        Self::collect_interface_methods(program, &mut self.interface_methods);
220        self.collect_type_aliases(program);
221
222        for sn in program {
223            match &sn.node {
224                Node::ImportDecl { .. } | Node::SelectiveImport { .. } => {
225                    self.compile_node(sn)?;
226                }
227                _ => {}
228            }
229        }
230        let main = program
231            .iter()
232            .find(|sn| matches!(peel_node(sn), Node::Pipeline { name, .. } if name == "default"))
233            .or_else(|| {
234                program
235                    .iter()
236                    .find(|sn| matches!(peel_node(sn), Node::Pipeline { .. }))
237            });
238
239        // When a pipeline body produces a final value, that value flows
240        // out of `vm.execute()` so the CLI can map it to a process exit
241        // code (int → exit n, Result::Err(msg) → stderr+exit 1).
242        let mut pipeline_emits_value = false;
243        if let Some(sn) = main {
244            self.compile_top_level_declarations(program)?;
245            if let Node::Pipeline { body, extends, .. } = peel_node(sn) {
246                if let Some(parent_name) = extends {
247                    self.compile_parent_pipeline(program, parent_name)?;
248                }
249                let saved = std::mem::replace(&mut self.module_level, false);
250                self.compile_block(body)?;
251                self.module_level = saved;
252                pipeline_emits_value = true;
253            }
254        } else {
255            // Script mode: no pipeline found, treat top-level as implicit entry.
256            let top_level: Vec<&SNode> = program
257                .iter()
258                .filter(|sn| {
259                    !matches!(
260                        &sn.node,
261                        Node::ImportDecl { .. } | Node::SelectiveImport { .. }
262                    )
263                })
264                .collect();
265            for sn in &top_level {
266                self.compile_discarded_stmt(sn)?;
267            }
268            // E4.1 entrypoint convention: a top-level `fn main(harness: Harness)`
269            // is invoked automatically with the runtime-provided `harness`
270            // global. The typechecker rejects every other signature with
271            // HARN-NAM-101 so we don't need to re-validate the shape here.
272            if Self::has_top_level_fn_main(program) {
273                let harness_name = self.string_constant("harness");
274                self.chunk.emit_u16(Op::GetVar, harness_name, self.line);
275                self.emit_named_call("main", 1);
276                pipeline_emits_value = true;
277            }
278        }
279
280        self.drain_finallys_to_floor(0)?;
281        if !pipeline_emits_value {
282            self.chunk.emit(Op::Nil, self.line);
283        }
284        self.chunk.emit(Op::Return, self.line);
285        Ok(self.chunk)
286    }
287
288    /// True when the program declares a top-level `fn main(...)`. Drives the
289    /// auto-call wired by `compile()` for the new `main(harness: Harness)`
290    /// entrypoint convention.
291    fn has_top_level_fn_main(program: &[SNode]) -> bool {
292        program
293            .iter()
294            .any(|sn| matches!(peel_node(sn), Node::FnDecl { name, .. } if name == "main"))
295    }
296
297    /// Compile a specific named pipeline (for test runners).
298    pub fn compile_named(
299        mut self,
300        program: &[SNode],
301        pipeline_name: &str,
302    ) -> Result<Chunk, CompileError> {
303        Self::collect_enum_names(program, &mut self.enum_names);
304        self.enum_names.insert("Result".to_string());
305        Self::collect_struct_layouts(program, &mut self.struct_layouts);
306        Self::collect_interface_methods(program, &mut self.interface_methods);
307        self.collect_type_aliases(program);
308
309        for sn in program {
310            if matches!(
311                &sn.node,
312                Node::ImportDecl { .. } | Node::SelectiveImport { .. }
313            ) {
314                self.compile_node(sn)?;
315            }
316        }
317        let target = program.iter().find(
318            |sn| matches!(peel_node(sn), Node::Pipeline { name, .. } if name == pipeline_name),
319        );
320
321        if let Some(sn) = target {
322            self.compile_top_level_declarations(program)?;
323            if let Node::Pipeline { body, extends, .. } = peel_node(sn) {
324                if let Some(parent_name) = extends {
325                    self.compile_parent_pipeline(program, parent_name)?;
326                }
327                let saved = std::mem::replace(&mut self.module_level, false);
328                self.compile_block(body)?;
329                self.module_level = saved;
330            }
331        }
332
333        self.drain_finallys_to_floor(0)?;
334        self.chunk.emit(Op::Nil, self.line);
335        self.chunk.emit(Op::Return, self.line);
336        Ok(self.chunk)
337    }
338
339    /// Recursively compile parent pipeline bodies (for extends).
340    pub(super) fn compile_parent_pipeline(
341        &mut self,
342        program: &[SNode],
343        parent_name: &str,
344    ) -> Result<(), CompileError> {
345        let parent = program
346            .iter()
347            .find(|sn| matches!(&sn.node, Node::Pipeline { name, .. } if name == parent_name));
348        if let Some(sn) = parent {
349            if let Node::Pipeline { body, extends, .. } = &sn.node {
350                if let Some(grandparent) = extends {
351                    self.compile_parent_pipeline(program, grandparent)?;
352                }
353                for stmt in body {
354                    self.compile_discarded_stmt(stmt)?;
355                }
356            }
357        }
358        Ok(())
359    }
360
361    /// Emit bytecode preamble for default parameter values.
362    /// For each param with a default at index i, emits:
363    ///   GetArgc; PushInt (i+1); GreaterEqual; JumpIfTrue <skip>;
364    ///   [compile default expr]; DefLet param_name; <skip>:
365    pub(super) fn emit_default_preamble(
366        &mut self,
367        params: &[TypedParam],
368    ) -> Result<(), CompileError> {
369        for (i, param) in params.iter().enumerate() {
370            if let Some(default_expr) = &param.default_value {
371                self.chunk.emit(Op::GetArgc, self.line);
372                let threshold_idx = self.chunk.add_constant(Constant::Int((i + 1) as i64));
373                self.chunk.emit_u16(Op::Constant, threshold_idx, self.line);
374                self.chunk.emit(Op::GreaterEqual, self.line);
375                let skip_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
376                // JumpIfTrue doesn't pop its boolean operand.
377                self.chunk.emit(Op::Pop, self.line);
378                // Compile the default with this param and all *later* params
379                // hidden from local resolution. A default is evaluated left to
380                // right at call time: it may reference an earlier parameter,
381                // but a mention of its own name (or a later, not-yet-bound
382                // parameter) must resolve to the enclosing scope — e.g.
383                // `let n = 7; fn f(n = n * 2)` reads the outer `n`. Without the
384                // mask, `n` bound to the param's own unset slot and threw at
385                // runtime. Earlier params stay visible.
386                let masked = self.mask_param_names(&params[i..]);
387                let result = self.compile_node(default_expr);
388                self.restore_param_names(masked);
389                result?;
390                self.emit_init_or_define_binding(&param.name, false);
391                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
392                self.chunk.patch_jump(skip_jump);
393                self.chunk.emit(Op::Pop, self.line);
394                self.chunk.patch_jump(end_jump);
395            }
396        }
397        Ok(())
398    }
399
400    /// Emit body-local type checks that call-site validation cannot cover.
401    /// Ordinary supplied arguments are validated by precomputed
402    /// [`crate::chunk::ParamSlot`] guards before the frame is entered. The
403    /// bytecode preamble still checks interface parameters, because interface
404    /// satisfaction depends on compiler-collected method metadata, and checks
405    /// defaulted schema parameters only when the caller omitted that argument.
406    pub(super) fn emit_type_checks(&mut self, params: &[TypedParam]) {
407        for (param_index, param) in params.iter().enumerate() {
408            if let Some(type_expr) = &param.type_expr {
409                let check_type = if param.rest {
410                    harn_parser::TypeExpr::List(Box::new(type_expr.clone()))
411                } else {
412                    type_expr.clone()
413                };
414
415                if let harn_parser::TypeExpr::Named(name) = &check_type {
416                    if let Some(methods) = self.interface_methods.get(name).cloned() {
417                        let fn_idx = self.string_constant("__assert_interface");
418                        self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
419                        self.emit_get_binding(&param.name);
420                        let name_idx = self.string_constant(&param.name);
421                        self.chunk.emit_u16(Op::Constant, name_idx, self.line);
422                        let iface_idx = self.string_constant(name);
423                        self.chunk.emit_u16(Op::Constant, iface_idx, self.line);
424                        let methods_str = methods.join(",");
425                        let methods_idx = self.owned_string_constant(methods_str);
426                        self.chunk.emit_u16(Op::Constant, methods_idx, self.line);
427                        self.chunk.emit_u8(Op::Call, 4, self.line);
428                        self.chunk.emit(Op::Pop, self.line);
429                        continue;
430                    }
431                }
432
433                if param.default_value.is_some() {
434                    if let Some(schema) = Self::type_expr_to_schema_value(&check_type) {
435                        self.emit_default_param_schema_check(param_index, param, &schema);
436                    }
437                }
438            }
439        }
440    }
441
442    fn emit_default_param_schema_check(
443        &mut self,
444        param_index: usize,
445        param: &TypedParam,
446        schema: &VmValue,
447    ) {
448        self.chunk.emit(Op::GetArgc, self.line);
449        let threshold_idx = self
450            .chunk
451            .add_constant(Constant::Int((param_index + 1) as i64));
452        self.chunk.emit_u16(Op::Constant, threshold_idx, self.line);
453        self.chunk.emit(Op::GreaterEqual, self.line);
454        let supplied_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
455        self.chunk.emit(Op::Pop, self.line);
456        self.emit_schema_assert_call(param, schema);
457        let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
458        self.chunk.patch_jump(supplied_jump);
459        self.chunk.emit(Op::Pop, self.line);
460        self.chunk.patch_jump(end_jump);
461    }
462
463    fn emit_schema_assert_call(&mut self, param: &TypedParam, schema: &VmValue) {
464        let fn_idx = self.string_constant("__assert_schema");
465        self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
466        self.emit_get_binding(&param.name);
467        let name_idx = self.string_constant(&param.name);
468        self.chunk.emit_u16(Op::Constant, name_idx, self.line);
469        self.emit_vm_value_literal(schema);
470        self.chunk.emit_u8(Op::Call, 3, self.line);
471        self.chunk.emit(Op::Pop, self.line);
472    }
473
474    pub(crate) fn type_expr_to_schema_value(type_expr: &harn_parser::TypeExpr) -> Option<VmValue> {
475        match type_expr {
476            harn_parser::TypeExpr::Named(name) => match name.as_str() {
477                "int" | "float" | "string" | "bool" | "list" | "dict" | "set" | "nil"
478                | "closure" | "bytes" => {
479                    Some(VmValue::Dict(std::sync::Arc::new(BTreeMap::from([(
480                        "type".to_string(),
481                        VmValue::String(std::sync::Arc::from(name.as_str())),
482                    )]))))
483                }
484                _ => None,
485            },
486            harn_parser::TypeExpr::Shape(fields)
487            | harn_parser::TypeExpr::OpenShape { fields, .. } => {
488                let mut properties = BTreeMap::new();
489                let mut required = Vec::new();
490                for field in fields {
491                    let field_schema = Self::type_expr_to_schema_value(&field.type_expr)?;
492                    properties.insert(field.name.clone(), field_schema);
493                    if !field.optional {
494                        required.push(VmValue::String(std::sync::Arc::from(field.name.as_str())));
495                    }
496                }
497                let mut out = BTreeMap::new();
498                out.insert(
499                    "type".to_string(),
500                    VmValue::String(std::sync::Arc::from("dict")),
501                );
502                out.insert(
503                    "properties".to_string(),
504                    VmValue::Dict(std::sync::Arc::new(properties)),
505                );
506                if !required.is_empty() {
507                    out.insert(
508                        "required".to_string(),
509                        VmValue::List(std::sync::Arc::new(required)),
510                    );
511                }
512                Some(VmValue::Dict(std::sync::Arc::new(out)))
513            }
514            harn_parser::TypeExpr::List(inner) => {
515                let mut out = BTreeMap::new();
516                out.insert(
517                    "type".to_string(),
518                    VmValue::String(std::sync::Arc::from("list")),
519                );
520                if let Some(item_schema) = Self::type_expr_to_schema_value(inner) {
521                    out.insert("items".to_string(), item_schema);
522                }
523                Some(VmValue::Dict(std::sync::Arc::new(out)))
524            }
525            harn_parser::TypeExpr::DictType(key, value) => {
526                let mut out = BTreeMap::new();
527                out.insert(
528                    "type".to_string(),
529                    VmValue::String(std::sync::Arc::from("dict")),
530                );
531                if matches!(key.as_ref(), harn_parser::TypeExpr::Named(name) if name == "string") {
532                    if let Some(value_schema) = Self::type_expr_to_schema_value(value) {
533                        out.insert("additional_properties".to_string(), value_schema);
534                    }
535                }
536                Some(VmValue::Dict(std::sync::Arc::new(out)))
537            }
538            harn_parser::TypeExpr::Union(members) => {
539                // Special-case unions of literals: emit as `enum: [...]`
540                // so the schema round-trips as canonical JSON Schema and
541                // is ACP-/OpenAPI-compatible. Mixed unions fall back to
542                // the `union:` key that validators recognize.
543                if !members.is_empty()
544                    && members
545                        .iter()
546                        .all(|m| matches!(m, harn_parser::TypeExpr::LitString(_)))
547                {
548                    let values = members
549                        .iter()
550                        .map(|m| match m {
551                            harn_parser::TypeExpr::LitString(s) => {
552                                VmValue::String(std::sync::Arc::from(s.as_str()))
553                            }
554                            _ => unreachable!(),
555                        })
556                        .collect::<Vec<_>>();
557                    return Some(VmValue::Dict(std::sync::Arc::new(BTreeMap::from([
558                        (
559                            "type".to_string(),
560                            VmValue::String(std::sync::Arc::from("string")),
561                        ),
562                        (
563                            "enum".to_string(),
564                            VmValue::List(std::sync::Arc::new(values)),
565                        ),
566                    ]))));
567                }
568                if !members.is_empty()
569                    && members
570                        .iter()
571                        .all(|m| matches!(m, harn_parser::TypeExpr::LitInt(_)))
572                {
573                    let values = members
574                        .iter()
575                        .map(|m| match m {
576                            harn_parser::TypeExpr::LitInt(v) => VmValue::Int(*v),
577                            _ => unreachable!(),
578                        })
579                        .collect::<Vec<_>>();
580                    return Some(VmValue::Dict(std::sync::Arc::new(BTreeMap::from([
581                        (
582                            "type".to_string(),
583                            VmValue::String(std::sync::Arc::from("int")),
584                        ),
585                        (
586                            "enum".to_string(),
587                            VmValue::List(std::sync::Arc::new(values)),
588                        ),
589                    ]))));
590                }
591                let branches = members
592                    .iter()
593                    .filter_map(Self::type_expr_to_schema_value)
594                    .collect::<Vec<_>>();
595                if branches.is_empty() {
596                    None
597                } else {
598                    Some(VmValue::Dict(std::sync::Arc::new(BTreeMap::from([(
599                        "union".to_string(),
600                        VmValue::List(std::sync::Arc::new(branches)),
601                    )]))))
602                }
603            }
604            harn_parser::TypeExpr::Intersection(members) => {
605                // Encode `A & B` as JSON-Schema `allOf` (the runtime
606                // accepts the snake_case `all_of` key directly). The
607                // value must validate against every branch.
608                let branches = members
609                    .iter()
610                    .filter_map(Self::type_expr_to_schema_value)
611                    .collect::<Vec<_>>();
612                if branches.is_empty() {
613                    None
614                } else {
615                    Some(VmValue::Dict(std::sync::Arc::new(BTreeMap::from([(
616                        "all_of".to_string(),
617                        VmValue::List(std::sync::Arc::new(branches)),
618                    )]))))
619                }
620            }
621            harn_parser::TypeExpr::FnType { .. } => {
622                Some(VmValue::Dict(std::sync::Arc::new(BTreeMap::from([(
623                    "type".to_string(),
624                    VmValue::String(std::sync::Arc::from("closure")),
625                )]))))
626            }
627            harn_parser::TypeExpr::Applied { .. } => None,
628            harn_parser::TypeExpr::Iter(_)
629            | harn_parser::TypeExpr::Generator(_)
630            | harn_parser::TypeExpr::Stream(_) => None,
631            harn_parser::TypeExpr::Never => None,
632            harn_parser::TypeExpr::LitString(s) => {
633                Some(VmValue::Dict(std::sync::Arc::new(BTreeMap::from([
634                    (
635                        "type".to_string(),
636                        VmValue::String(std::sync::Arc::from("string")),
637                    ),
638                    (
639                        "const".to_string(),
640                        VmValue::String(std::sync::Arc::from(s.as_str())),
641                    ),
642                ]))))
643            }
644            harn_parser::TypeExpr::LitInt(v) => {
645                Some(VmValue::Dict(std::sync::Arc::new(BTreeMap::from([
646                    (
647                        "type".to_string(),
648                        VmValue::String(std::sync::Arc::from("int")),
649                    ),
650                    ("const".to_string(), VmValue::Int(*v)),
651                ]))))
652            }
653            harn_parser::TypeExpr::Owned(inner) => Self::type_expr_to_schema_value(inner),
654        }
655    }
656
657    pub(super) fn emit_vm_value_literal(&mut self, value: &VmValue) {
658        match value {
659            VmValue::String(text) => {
660                let idx = self.string_constant(text);
661                self.chunk.emit_u16(Op::Constant, idx, self.line);
662            }
663            VmValue::Int(number) => {
664                let idx = self.chunk.add_constant(Constant::Int(*number));
665                self.chunk.emit_u16(Op::Constant, idx, self.line);
666            }
667            VmValue::Float(number) => {
668                let idx = self.chunk.add_constant(Constant::Float(*number));
669                self.chunk.emit_u16(Op::Constant, idx, self.line);
670            }
671            VmValue::Bool(value) => {
672                let idx = self.chunk.add_constant(Constant::Bool(*value));
673                self.chunk.emit_u16(Op::Constant, idx, self.line);
674            }
675            VmValue::Nil => self.chunk.emit(Op::Nil, self.line),
676            VmValue::List(items) => {
677                for item in items.iter() {
678                    self.emit_vm_value_literal(item);
679                }
680                self.chunk
681                    .emit_u16(Op::BuildList, items.len() as u16, self.line);
682            }
683            VmValue::Dict(entries) => {
684                for (key, item) in entries.iter() {
685                    let key_idx = self.string_constant(key);
686                    self.chunk.emit_u16(Op::Constant, key_idx, self.line);
687                    self.emit_vm_value_literal(item);
688                }
689                self.chunk
690                    .emit_u16(Op::BuildDict, entries.len() as u16, self.line);
691            }
692            _ => {}
693        }
694    }
695
696    /// Emit the extra u16 type name index after a TryCatchSetup jump.
697    pub(super) fn emit_type_name_extra(&mut self, type_name_idx: u16) {
698        let hi = (type_name_idx >> 8) as u8;
699        let lo = type_name_idx as u8;
700        self.chunk.code.push(hi);
701        self.chunk.code.push(lo);
702        self.chunk.lines.push(self.line);
703        self.chunk.columns.push(self.column);
704        self.chunk.lines.push(self.line);
705        self.chunk.columns.push(self.column);
706    }
707
708    /// Compile a try/catch body block (produces a value on the stack).
709    pub(super) fn compile_try_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
710        if body.is_empty() {
711            self.chunk.emit(Op::Nil, self.line);
712        } else {
713            self.compile_scoped_block(body)?;
714        }
715        Ok(())
716    }
717
718    /// Compile catch error binding (error value is on stack from handler).
719    pub(super) fn compile_catch_binding(
720        &mut self,
721        error_var: &Option<String>,
722    ) -> Result<(), CompileError> {
723        if let Some(var_name) = error_var {
724            self.emit_define_binding(var_name, false);
725        } else {
726            self.chunk.emit(Op::Pop, self.line);
727        }
728        Ok(())
729    }
730
731    /// Compile finally body inline, discarding its result value.
732    /// `compile_scoped_block` always leaves exactly one value on the stack
733    /// (Nil for non-value tail statements), so the trailing Pop is
734    /// unconditional — otherwise a finally ending in e.g. `x = x + 1`
735    /// would leave a stray Nil that corrupts the surrounding expression
736    /// when the enclosing try/finally is used in expression position.
737    pub(super) fn compile_finally_inline(
738        &mut self,
739        finally_body: &[SNode],
740    ) -> Result<(), CompileError> {
741        if !finally_body.is_empty() {
742            self.compile_scoped_block(finally_body)?;
743            self.chunk.emit(Op::Pop, self.line);
744        }
745        Ok(())
746    }
747
748    /// Collect pending finally bodies from the top of the stack down to
749    /// (but not including) the innermost `CatchBarrier`. Used by `throw`
750    /// lowering: throws caught locally don't unwind past the catch, so
751    /// finallys behind the barrier aren't on the throw's exit path.
752    pub(super) fn pending_finallys_until_barrier(&self) -> Vec<Vec<SNode>> {
753        let mut out = Vec::new();
754        for entry in self.finally_bodies.iter().rev() {
755            match entry {
756                FinallyEntry::CatchBarrier => break,
757                FinallyEntry::Finally(body) => out.push(body.clone()),
758            }
759        }
760        out
761    }
762
763    /// True if there are any pending finally bodies (not just barriers).
764    pub(super) fn has_pending_finally(&self) -> bool {
765        self.finally_bodies
766            .iter()
767            .any(|e| matches!(e, FinallyEntry::Finally(_)))
768    }
769
770    /// Save a thrown value to a temp and rethrow without running finally.
771    ///
772    /// Historically this helper also invoked `compile_finally_inline` on the
773    /// thrown path, but that produced observable double-runs: the
774    /// `Node::ThrowStmt` lowering (below) already iterates `finally_bodies`
775    /// and runs each pending finally inline *before* emitting `Op::Throw`, so
776    /// a second run here fired the same side effects twice. Finally now runs
777    /// exactly once — via the throw-emit path during unwinding.
778    pub(super) fn compile_plain_rethrow(&mut self) -> Result<(), CompileError> {
779        self.temp_counter += 1;
780        let temp_name = format!("__finally_err_{}__", self.temp_counter);
781        self.emit_define_binding(&temp_name, true);
782        self.emit_get_binding(&temp_name);
783        self.chunk.emit(Op::Throw, self.line);
784        Ok(())
785    }
786
787    pub(super) fn declare_param_slots(&mut self, params: &[TypedParam]) {
788        for param in params {
789            self.define_local_slot(&param.name, false);
790        }
791    }
792
793    /// Temporarily remove the given parameters' names from the innermost local
794    /// scope so that, while compiling a default-value expression, references to
795    /// them resolve to the enclosing scope instead of their not-yet-bound param
796    /// slots. Returns the removed bindings so [`Self::restore_param_names`] can
797    /// reinstate them afterward. See [`Self::emit_default_preamble`].
798    fn mask_param_names(&mut self, params: &[TypedParam]) -> Vec<(String, super::LocalBinding)> {
799        let mut removed = Vec::new();
800        if let Some(scope) = self.local_scopes.last_mut() {
801            for param in params {
802                if let Some(binding) = scope.remove(&param.name) {
803                    removed.push((param.name.clone(), binding));
804                }
805            }
806        }
807        removed
808    }
809
810    /// Reinstate parameter names removed by [`Self::mask_param_names`].
811    fn restore_param_names(&mut self, removed: Vec<(String, super::LocalBinding)>) {
812        if let Some(scope) = self.local_scopes.last_mut() {
813            for (name, binding) in removed {
814                scope.insert(name, binding);
815            }
816        }
817    }
818
819    fn define_local_slot(&mut self, name: &str, mutable: bool) -> Option<u16> {
820        if self.module_level || harn_parser::is_discard_name(name) {
821            return None;
822        }
823        let current = self.local_scopes.last_mut()?;
824        if let Some(existing) = current.get_mut(name) {
825            if existing.mutable || mutable {
826                if mutable {
827                    existing.mutable = true;
828                    if let Some(info) = self.chunk.local_slots.get_mut(existing.slot as usize) {
829                        info.mutable = true;
830                    }
831                }
832                return Some(existing.slot);
833            }
834            return None;
835        }
836        let slot = self
837            .chunk
838            .add_local_slot(name.to_string(), mutable, self.scope_depth);
839        current.insert(name.to_string(), super::LocalBinding { slot, mutable });
840        Some(slot)
841    }
842
843    pub(super) fn resolve_local_slot(&self, name: &str) -> Option<super::LocalBinding> {
844        if self.module_level {
845            return None;
846        }
847        self.local_scopes
848            .iter()
849            .rev()
850            .find_map(|scope| scope.get(name).copied())
851    }
852
853    pub(super) fn emit_get_binding(&mut self, name: &str) {
854        if let Some(binding) = self.resolve_local_slot(name) {
855            self.chunk
856                .emit_u16(Op::GetLocalSlot, binding.slot, self.line);
857        } else {
858            let idx = self.string_constant(name);
859            self.chunk.emit_u16(Op::GetVar, idx, self.line);
860        }
861    }
862
863    pub(super) fn emit_define_binding(&mut self, name: &str, mutable: bool) {
864        if let Some(slot) = self.define_local_slot(name, mutable) {
865            self.chunk.emit_u16(Op::DefLocalSlot, slot, self.line);
866        } else {
867            let idx = self.string_constant(name);
868            let op = if mutable { Op::DefVar } else { Op::DefLet };
869            self.chunk.emit_u16(op, idx, self.line);
870        }
871    }
872
873    pub(super) fn emit_init_or_define_binding(&mut self, name: &str, mutable: bool) {
874        if let Some(binding) = self.resolve_local_slot(name) {
875            self.chunk
876                .emit_u16(Op::DefLocalSlot, binding.slot, self.line);
877        } else {
878            self.emit_define_binding(name, mutable);
879        }
880    }
881
882    pub(super) fn emit_set_binding(&mut self, name: &str) {
883        if let Some(binding) = self.resolve_local_slot(name) {
884            let _ = binding.mutable;
885            self.chunk
886                .emit_u16(Op::SetLocalSlot, binding.slot, self.line);
887        } else {
888            let idx = self.string_constant(name);
889            self.chunk.emit_u16(Op::SetVar, idx, self.line);
890        }
891    }
892
893    pub(super) fn begin_scope(&mut self) {
894        self.chunk.emit(Op::PushScope, self.line);
895        self.scope_depth += 1;
896        self.type_scopes.push(std::collections::HashMap::new());
897        self.local_scopes.push(std::collections::HashMap::new());
898    }
899
900    pub(super) fn end_scope(&mut self) {
901        if self.scope_depth > 0 {
902            self.chunk.emit(Op::PopScope, self.line);
903            self.scope_depth -= 1;
904            self.type_scopes.pop();
905            self.local_scopes.pop();
906        }
907    }
908
909    /// Emit cleanup for an abrupt control-flow path without changing the
910    /// compiler's lexical scope stacks for the source path that follows it.
911    pub(super) fn emit_scope_unwind_to(&mut self, target_depth: usize) {
912        for _ in target_depth..self.scope_depth {
913            self.chunk.emit(Op::PopScope, self.line);
914        }
915    }
916
917    pub(super) fn compile_scoped_block(&mut self, stmts: &[SNode]) -> Result<(), CompileError> {
918        self.begin_scope();
919        let finally_floor = self.finally_bodies.len();
920        if stmts.is_empty() {
921            self.chunk.emit(Op::Nil, self.line);
922        } else {
923            self.compile_block(stmts)?;
924        }
925        self.drain_finallys_to_floor(finally_floor)?;
926        self.end_scope();
927        Ok(())
928    }
929
930    pub(super) fn compile_scoped_statements(
931        &mut self,
932        stmts: &[SNode],
933    ) -> Result<(), CompileError> {
934        self.begin_scope();
935        self.record_monomorphic_var_bindings(stmts);
936        let finally_floor = self.finally_bodies.len();
937        for sn in stmts {
938            self.compile_discarded_stmt(sn)?;
939        }
940        self.drain_finallys_to_floor(finally_floor)?;
941        self.end_scope();
942        Ok(())
943    }
944
945    /// Drain pending `defer` bodies down to a saved floor and run each inline
946    /// in LIFO order. Each defer body is popped *before* its code is emitted so
947    /// any `return` / `break` lowering inside the body sees the remaining
948    /// pending defers (not itself).
949    pub(super) fn drain_finallys_to_floor(&mut self, floor: usize) -> Result<(), CompileError> {
950        while self.finally_bodies.len() > floor {
951            let entry = self.finally_bodies.pop().expect("non-empty by guard");
952            if let FinallyEntry::Finally(body) = entry {
953                self.compile_finally_inline(&body)?;
954            }
955        }
956        Ok(())
957    }
958
959    /// Run the pending finally/defer bodies a non-local transfer (`return`,
960    /// `break`, `continue`) crosses on its way down to `floor`, innermost
961    /// first, then restore the pending stack.
962    ///
963    /// Like [`Self::drain_finallys_to_floor`] each body is removed from the
964    /// stack *before* it is inlined, so a `return`/`break`/`continue` inside a
965    /// finally body runs only the finallys *outside* it instead of re-running
966    /// the one it is in — which otherwise recursed forever at compile time and
967    /// aborted the process with a stack overflow. Unlike that helper (used at
968    /// scope exit), the stack is restored afterward because a transfer is a
969    /// branch: the code the compiler emits after it still needs the pending
970    /// finallys for the fall-through and sibling paths.
971    pub(super) fn run_pending_finallys_for_transfer(
972        &mut self,
973        floor: usize,
974    ) -> Result<(), CompileError> {
975        if self.finally_bodies.len() <= floor {
976            return Ok(());
977        }
978        let saved = self.finally_bodies[floor..].to_vec();
979        let result = self.drain_finallys_to_floor(floor);
980        self.finally_bodies.extend(saved);
981        result
982    }
983
984    /// Like [`Self::run_pending_finallys_for_transfer`] but for a `throw`: run
985    /// only the finallys between here and the innermost `CatchBarrier` (the
986    /// ones the unwind actually crosses before a local `catch` halts it),
987    /// masking each while it is inlined and restoring the stack afterward.
988    pub(super) fn run_pending_finallys_until_barrier(&mut self) -> Result<(), CompileError> {
989        let floor = self
990            .finally_bodies
991            .iter()
992            .rposition(|e| matches!(e, FinallyEntry::CatchBarrier))
993            .map(|i| i + 1)
994            .unwrap_or(0);
995        self.run_pending_finallys_for_transfer(floor)
996    }
997
998    /// Register an auto-drop defer for an `owned<T>` binding. The drop runs
999    /// at scope exit alongside any user-written `defer { ... }` blocks (LIFO
1000    /// order) and on `return` / `break` / `continue` / `throw` via the
1001    /// existing finally-unwinding machinery.
1002    pub(super) fn maybe_register_owned_drop(
1003        &mut self,
1004        pattern: &harn_parser::BindingPattern,
1005        type_ann: Option<&TypeExpr>,
1006        span: harn_lexer::Span,
1007    ) {
1008        // Auto-drop only fires when the user explicitly opted in via
1009        // `owned<T>` on a single-identifier binding. Destructured patterns
1010        // (`{a, b}`, `[a, b]`, pairs) aren't auto-dropped: ownership of a
1011        // composite isn't well-defined, and users can wrap individual fields
1012        // with `owned<T>` and bind them separately if needed.
1013        let Some(ty) = type_ann else {
1014            return;
1015        };
1016        if !matches!(ty, TypeExpr::Owned(_)) {
1017            return;
1018        }
1019        let harn_parser::BindingPattern::Identifier(name) = pattern else {
1020            return;
1021        };
1022        if harn_parser::is_discard_name(name) {
1023            return;
1024        }
1025        let call = harn_parser::spanned(
1026            Node::FunctionCall {
1027                name: "drop".to_string(),
1028                args: vec![harn_parser::spanned(Node::Identifier(name.clone()), span)],
1029                type_args: Vec::new(),
1030            },
1031            span,
1032        );
1033        self.finally_bodies.push(FinallyEntry::Finally(vec![call]));
1034    }
1035
1036    /// Compile a statement that appears in a value-discarding sequence —
1037    /// the script-mode module body, an inherited pipeline body, and block
1038    /// interiors — then pop its value when `produces_value` says it left
1039    /// one.
1040    ///
1041    /// In debug builds this also asserts the operand stack stayed balanced
1042    /// across the statement: a straight-line statement must net exactly one
1043    /// value when `produces_value` is true and zero otherwise. That turns a
1044    /// `produces_value` misclassification — like the attributed-decl gap
1045    /// fixed in #2610, where the loop popped against an empty stack — from a
1046    /// latent runtime "Stack underflow" (often masked further by the
1047    /// bytecode cache, #2621) into a loud compile-time failure in tests/CI.
1048    /// Statements containing branches or other non-linearly-modeled opcodes
1049    /// can't be summed by the lightweight model, so the assertion skips them
1050    /// (see [`Chunk::balance_delta_since`]).
1051    pub(super) fn compile_discarded_stmt(&mut self, sn: &SNode) -> Result<(), CompileError> {
1052        #[cfg(debug_assertions)]
1053        let probe = self.chunk.balance_probe();
1054        self.compile_node(sn)?;
1055        #[allow(unused_mut)]
1056        let mut produces = Self::produces_value(&sn.node);
1057        // Test-only hook: deliberately miswire the classification to prove
1058        // the balance assertion below trips on a `produces_value` gap (the
1059        // #2622 verification). No-op in non-test builds.
1060        #[cfg(test)]
1061        if let Some(forced) = FORCE_DISCARDED_PRODUCES_VALUE.with(std::cell::Cell::get) {
1062            produces = forced;
1063        }
1064        #[cfg(debug_assertions)]
1065        if let Some(delta) = self.chunk.balance_delta_since(probe) {
1066            let expected = i32::from(produces);
1067            debug_assert_eq!(
1068                delta, expected,
1069                "operand-stack imbalance at line {}: produces_value={produces} but the \
1070                 node's emitted bytecode netted {delta} (expected {expected}). A \
1071                 `produces_value` arm is out of sync with this node's codegen — see #2622.\n\
1072                 node: {:?}",
1073                self.line, sn.node,
1074            );
1075        }
1076        if produces {
1077            self.chunk.emit(Op::Pop, self.line);
1078        }
1079        Ok(())
1080    }
1081
1082    pub(super) fn compile_block(&mut self, stmts: &[SNode]) -> Result<(), CompileError> {
1083        self.record_monomorphic_var_bindings(stmts);
1084        for (i, snode) in stmts.iter().enumerate() {
1085            if i == stmts.len() - 1 {
1086                // The block's value is its last statement's. Backfill a `Nil`
1087                // when that statement produced none, so the block always
1088                // leaves exactly one value on the stack.
1089                self.compile_node(snode)?;
1090                if !Self::produces_value(&snode.node) {
1091                    self.chunk.emit(Op::Nil, self.line);
1092                }
1093            } else {
1094                self.compile_discarded_stmt(snode)?;
1095            }
1096        }
1097        Ok(())
1098    }
1099
1100    /// Compile a match arm body, ensuring it always pushes exactly one value.
1101    pub(super) fn compile_match_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
1102        self.begin_scope();
1103        let finally_floor = self.finally_bodies.len();
1104        if body.is_empty() {
1105            self.chunk.emit(Op::Nil, self.line);
1106        } else {
1107            self.compile_block(body)?;
1108            if !Self::produces_value(&body.last().unwrap().node) {
1109                self.chunk.emit(Op::Nil, self.line);
1110            }
1111        }
1112        self.drain_finallys_to_floor(finally_floor)?;
1113        self.end_scope();
1114        Ok(())
1115    }
1116
1117    /// Emit the binary op instruction for a compound assignment operator.
1118    pub(super) fn emit_compound_op(&mut self, op: &str) -> Result<(), CompileError> {
1119        match op {
1120            "+" => self.chunk.emit(Op::Add, self.line),
1121            "-" => self.chunk.emit(Op::Sub, self.line),
1122            "*" => self.chunk.emit(Op::Mul, self.line),
1123            "/" => self.chunk.emit(Op::Div, self.line),
1124            "%" => self.chunk.emit(Op::Mod, self.line),
1125            _ => {
1126                return Err(CompileError {
1127                    message: format!("Unknown compound operator: {op}"),
1128                    line: self.line,
1129                })
1130            }
1131        }
1132        Ok(())
1133    }
1134
1135    /// Extract the root variable name from a (possibly nested) access expression.
1136    pub(super) fn root_var_name(&self, node: &SNode) -> Option<String> {
1137        match &node.node {
1138            Node::Identifier(name) => Some(name.clone()),
1139            Node::PropertyAccess { object, .. } | Node::OptionalPropertyAccess { object, .. } => {
1140                self.root_var_name(object)
1141            }
1142            Node::SubscriptAccess { object, .. } | Node::OptionalSubscriptAccess { object, .. } => {
1143                self.root_var_name(object)
1144            }
1145            _ => None,
1146        }
1147    }
1148
1149    pub(super) fn compile_top_level_declarations(
1150        &mut self,
1151        program: &[SNode],
1152    ) -> Result<(), CompileError> {
1153        // Phase 1: evaluate module-level `let` / `var` bindings first, in
1154        // source order. This ensures function closures compiled in phase 2
1155        // capture these names in their env snapshot via `Op::Closure` —
1156        // fixing the "Undefined variable: FOO" surprise where a top-level
1157        // `let FOO = "..."` was silently dropped because it wasn't in this
1158        // match list. Keep in step with the import-time init path in
1159        // `crates/harn-vm/src/vm/imports.rs` (`module_state` construction).
1160        for sn in program {
1161            if matches!(
1162                &sn.node,
1163                Node::LetBinding { .. } | Node::VarBinding { .. } | Node::ConstBinding { .. }
1164            ) {
1165                self.compile_node(sn)?;
1166            }
1167        }
1168        // Phase 2: compile type and function declarations. Function closures
1169        // created here capture the current env which now includes the
1170        // module-level bindings from phase 1. Attributed declarations are
1171        // compiled here too — the AttributedDecl arm in compile_node
1172        // dispatches to the inner declaration's compile path.
1173        for sn in program {
1174            let inner_kind = match &sn.node {
1175                Node::AttributedDecl { inner, .. } => &inner.node,
1176                other => other,
1177            };
1178            match inner_kind {
1179                Node::EvalPackDecl {
1180                    binding_name,
1181                    pack_id,
1182                    fields,
1183                    body,
1184                    summarize,
1185                    ..
1186                } => {
1187                    self.compile_eval_pack_decl(
1188                        binding_name,
1189                        pack_id,
1190                        fields,
1191                        body,
1192                        summarize,
1193                        false,
1194                    )?;
1195                }
1196                Node::FnDecl { .. }
1197                | Node::ToolDecl { .. }
1198                | Node::SkillDecl { .. }
1199                | Node::ImplBlock { .. }
1200                | Node::StructDecl { .. }
1201                | Node::EnumDecl { .. }
1202                | Node::InterfaceDecl { .. }
1203                | Node::TypeDecl { .. } => {
1204                    self.compile_node(sn)?;
1205                }
1206                _ => {}
1207            }
1208        }
1209        Ok(())
1210    }
1211
1212    /// Recursively collect all enum type names from the AST.
1213    pub(super) fn collect_enum_names(
1214        nodes: &[SNode],
1215        names: &mut std::collections::HashSet<String>,
1216    ) {
1217        for sn in nodes {
1218            match &sn.node {
1219                Node::EnumDecl { name, .. } => {
1220                    names.insert(name.clone());
1221                }
1222                Node::Pipeline { body, .. } => {
1223                    Self::collect_enum_names(body, names);
1224                }
1225                Node::FnDecl { body, .. } | Node::ToolDecl { body, .. } => {
1226                    Self::collect_enum_names(body, names);
1227                }
1228                Node::SkillDecl { fields, .. } => {
1229                    for (_k, v) in fields {
1230                        Self::collect_enum_names(std::slice::from_ref(v), names);
1231                    }
1232                }
1233                Node::EvalPackDecl {
1234                    fields,
1235                    body,
1236                    summarize,
1237                    ..
1238                } => {
1239                    for (_k, v) in fields {
1240                        Self::collect_enum_names(std::slice::from_ref(v), names);
1241                    }
1242                    Self::collect_enum_names(body, names);
1243                    if let Some(summary_body) = summarize {
1244                        Self::collect_enum_names(summary_body, names);
1245                    }
1246                }
1247                Node::Block(stmts) => {
1248                    Self::collect_enum_names(stmts, names);
1249                }
1250                Node::AttributedDecl { inner, .. } => {
1251                    Self::collect_enum_names(std::slice::from_ref(inner), names);
1252                }
1253                _ => {}
1254            }
1255        }
1256    }
1257
1258    pub(super) fn collect_struct_layouts(
1259        nodes: &[SNode],
1260        layouts: &mut std::collections::HashMap<String, Vec<String>>,
1261    ) {
1262        for sn in nodes {
1263            match &sn.node {
1264                Node::StructDecl { name, fields, .. } => {
1265                    layouts.insert(
1266                        name.clone(),
1267                        fields.iter().map(|field| field.name.clone()).collect(),
1268                    );
1269                }
1270                Node::Pipeline { body, .. }
1271                | Node::FnDecl { body, .. }
1272                | Node::ToolDecl { body, .. } => {
1273                    Self::collect_struct_layouts(body, layouts);
1274                }
1275                Node::SkillDecl { fields, .. } => {
1276                    for (_k, v) in fields {
1277                        Self::collect_struct_layouts(std::slice::from_ref(v), layouts);
1278                    }
1279                }
1280                Node::EvalPackDecl {
1281                    fields,
1282                    body,
1283                    summarize,
1284                    ..
1285                } => {
1286                    for (_k, v) in fields {
1287                        Self::collect_struct_layouts(std::slice::from_ref(v), layouts);
1288                    }
1289                    Self::collect_struct_layouts(body, layouts);
1290                    if let Some(summary_body) = summarize {
1291                        Self::collect_struct_layouts(summary_body, layouts);
1292                    }
1293                }
1294                Node::Block(stmts) => {
1295                    Self::collect_struct_layouts(stmts, layouts);
1296                }
1297                Node::AttributedDecl { inner, .. } => {
1298                    Self::collect_struct_layouts(std::slice::from_ref(inner), layouts);
1299                }
1300                _ => {}
1301            }
1302        }
1303    }
1304
1305    pub(super) fn collect_interface_methods(
1306        nodes: &[SNode],
1307        interfaces: &mut std::collections::HashMap<String, Vec<String>>,
1308    ) {
1309        for sn in nodes {
1310            match &sn.node {
1311                Node::InterfaceDecl { name, methods, .. } => {
1312                    let method_names: Vec<String> =
1313                        methods.iter().map(|m| m.name.clone()).collect();
1314                    interfaces.insert(name.clone(), method_names);
1315                }
1316                Node::Pipeline { body, .. }
1317                | Node::FnDecl { body, .. }
1318                | Node::ToolDecl { body, .. } => {
1319                    Self::collect_interface_methods(body, interfaces);
1320                }
1321                Node::SkillDecl { fields, .. } => {
1322                    for (_k, v) in fields {
1323                        Self::collect_interface_methods(std::slice::from_ref(v), interfaces);
1324                    }
1325                }
1326                Node::EvalPackDecl {
1327                    fields,
1328                    body,
1329                    summarize,
1330                    ..
1331                } => {
1332                    for (_k, v) in fields {
1333                        Self::collect_interface_methods(std::slice::from_ref(v), interfaces);
1334                    }
1335                    Self::collect_interface_methods(body, interfaces);
1336                    if let Some(summary_body) = summarize {
1337                        Self::collect_interface_methods(summary_body, interfaces);
1338                    }
1339                }
1340                Node::Block(stmts) => {
1341                    Self::collect_interface_methods(stmts, interfaces);
1342                }
1343                Node::AttributedDecl { inner, .. } => {
1344                    Self::collect_interface_methods(std::slice::from_ref(inner), interfaces);
1345                }
1346                _ => {}
1347            }
1348        }
1349    }
1350
1351    /// Compile a function body into a CompiledFunction (for import support).
1352    ///
1353    /// This path is used when a module is imported and its top-level `fn`
1354    /// declarations are loaded into the importer's environment. It MUST emit
1355    /// the same function preamble as the in-file `Node::FnDecl` path, or
1356    /// imported functions will behave differently from locally-defined ones —
1357    /// in particular, default parameter values would never be set and typed
1358    /// parameters would not be runtime-checked.
1359    ///
1360    /// `source_file`, when provided, tags the resulting chunk so runtime
1361    /// errors can attribute frames to the imported file rather than the
1362    /// entry-point pipeline.
1363    pub fn compile_fn_body(
1364        &mut self,
1365        type_params: &[harn_parser::TypeParam],
1366        params: &[TypedParam],
1367        body: &[SNode],
1368        source_file: Option<String>,
1369    ) -> Result<CompiledFunction, CompileError> {
1370        let mut fn_compiler = self.nested_body();
1371        fn_compiler.enum_names = self.enum_names.clone();
1372        fn_compiler.interface_methods = self.interface_methods.clone();
1373        fn_compiler.type_aliases = self.type_aliases.clone();
1374        fn_compiler.struct_layouts = self.struct_layouts.clone();
1375        fn_compiler.declare_param_slots(params);
1376        fn_compiler.record_param_types(params);
1377        fn_compiler.emit_default_preamble(params)?;
1378        fn_compiler.emit_type_checks(params);
1379        let is_gen = body_contains_yield(body);
1380        fn_compiler.compile_block(body)?;
1381        fn_compiler.chunk.emit(Op::Nil, 0);
1382        fn_compiler.chunk.emit(Op::Return, 0);
1383        fn_compiler.chunk.source_file = source_file;
1384        let param_slots = crate::chunk::ParamSlot::vec_from_typed(params);
1385        let has_runtime_type_checks =
1386            CompiledFunction::has_runtime_type_checks_for_params(&param_slots);
1387        Ok(CompiledFunction {
1388            name: String::new(),
1389            type_params: type_params.iter().map(|param| param.name.clone()).collect(),
1390            nominal_type_names: fn_compiler.nominal_type_names(),
1391            params: param_slots,
1392            default_start: TypedParam::default_start(params),
1393            chunk: Arc::new(fn_compiler.chunk),
1394            is_generator: is_gen,
1395            is_stream: false,
1396            has_rest_param: false,
1397            has_runtime_type_checks,
1398        })
1399    }
1400
1401    /// Check if a node produces a value on the stack that needs to be popped.
1402    pub(super) fn produces_value(node: &Node) -> bool {
1403        match node {
1404            // An attribute decorates a declaration (fn/struct/enum/…), never
1405            // an expression — so an attributed top-level item is a statement
1406            // that leaves nothing on the operand stack, exactly like its bare
1407            // inner declaration. Classifying by the inner node prevents the
1408            // script-mode top-level loop from emitting a spurious `Pop` (which
1409            // underflows the stack) after compiling, e.g., a `@route pub fn`.
1410            Node::AttributedDecl { inner, .. } => Self::produces_value(&inner.node),
1411            Node::LetBinding { .. }
1412            | Node::VarBinding { .. }
1413            | Node::ConstBinding { .. }
1414            | Node::Assignment { .. }
1415            | Node::ReturnStmt { .. }
1416            | Node::FnDecl { .. }
1417            | Node::ToolDecl { .. }
1418            | Node::SkillDecl { .. }
1419            | Node::EvalPackDecl { .. }
1420            | Node::ImplBlock { .. }
1421            | Node::StructDecl { .. }
1422            | Node::EnumDecl { .. }
1423            | Node::InterfaceDecl { .. }
1424            | Node::TypeDecl { .. }
1425            // Metadata-only declarations that emit no bytecode — see the
1426            // matching arm in `compile_node`.
1427            | Node::OverrideDecl { .. }
1428            | Node::Pipeline { .. }
1429            | Node::ThrowStmt { .. }
1430            | Node::BreakStmt
1431            | Node::ContinueStmt
1432            | Node::RequireStmt { .. }
1433            | Node::DeferStmt { .. } => false,
1434            Node::TryCatch { has_catch: _, .. }
1435            | Node::TryExpr { .. }
1436            | Node::Retry { .. }
1437            | Node::GuardStmt { .. }
1438            | Node::DeadlineBlock { .. }
1439            | Node::MutexBlock { .. }
1440            | Node::Spread(_) => true,
1441            _ => true,
1442        }
1443    }
1444}
1445
1446impl Default for Compiler {
1447    fn default() -> Self {
1448        Self::new()
1449    }
1450}