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