Skip to main content

harn_vm/compiler/
state.rs

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