Skip to main content

harn_kernel/compiler/
state.rs

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