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