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