Skip to main content

harn_vm/compiler/
state.rs

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