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