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