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