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                | "json_decode"
462                | "schema_check"
463                | "schema_report"
464                | "is_type"
465                | "json_validate"
466        )
467    }
468
469    /// Check whether a dict-literal key node matches the given keyword
470    /// (identifier or string literal form).
471    pub(super) fn entry_key_is(key: &SNode, keyword: &str) -> bool {
472        matches!(
473            &key.node,
474            Node::Identifier(name) | Node::StringLiteral(name) | Node::RawStringLiteral(name)
475                if name == keyword
476        )
477    }
478
479    /// Compile a program (list of top-level nodes) into a Chunk.
480    /// Finds the entry pipeline and compiles its body, including inherited bodies.
481    pub fn compile(mut self, program: &[SNode]) -> Result<Chunk, CompileError> {
482        // Pre-scan so we can recognize EnumName.Variant as enum construction
483        // even when the enum is declared inside a pipeline.
484        self.prepare_module_context(program);
485
486        for sn in program {
487            match &sn.node {
488                Node::ImportDecl { .. }
489                | Node::SelectiveImport { .. }
490                | Node::NamespaceImport { .. } => {
491                    self.compile_node(sn)?;
492                }
493                _ => {}
494            }
495        }
496        let main = program
497            .iter()
498            .find(|sn| matches!(peel_node(sn), Node::Pipeline { name, .. } if name == "default"))
499            .or_else(|| {
500                program
501                    .iter()
502                    .find(|sn| matches!(peel_node(sn), Node::Pipeline { .. }))
503            });
504
505        // When a pipeline body produces a final value, that value flows
506        // out of `vm.execute()` so the CLI can map it to a process exit
507        // code (int → exit n, Result::Err(msg) → stderr+exit 1).
508        let mut pipeline_emits_value = false;
509        if let Some(sn) = main {
510            self.compile_top_level_declarations(program)?;
511            if let Node::Pipeline {
512                params,
513                body,
514                extends,
515                throws,
516                ..
517            } = peel_node(sn)
518            {
519                self.compile_with_pipeline_captures(
520                    program,
521                    body,
522                    extends.as_deref(),
523                    |compiler| {
524                        let saved = std::mem::replace(&mut compiler.module_level, false);
525                        let saved_throw =
526                            std::mem::replace(&mut compiler.declared_throw, throws.is_some());
527                        if let Some(harness) = params.first().filter(|param| {
528                            matches!(
529                                param.type_expr.as_ref(),
530                                Some(TypeExpr::Named(name)) if name == "Harness"
531                            )
532                        }) {
533                            compiler.chunk.emit(Op::RootHarness, compiler.line);
534                            compiler.emit_define_binding(&harness.name, false);
535                        }
536                        if let Some(parent_name) = extends {
537                            compiler.compile_parent_pipeline(program, parent_name)?;
538                        }
539                        let result = compiler.compile_block(body);
540                        compiler.module_level = saved;
541                        compiler.declared_throw = saved_throw;
542                        result
543                    },
544                )?;
545                pipeline_emits_value = true;
546            }
547        } else {
548            // Script mode: no pipeline found, treat top-level as implicit entry.
549            let top_level: Vec<&SNode> = program
550                .iter()
551                .filter(|sn| {
552                    !matches!(
553                        &sn.node,
554                        Node::ImportDecl { .. }
555                            | Node::SelectiveImport { .. }
556                            | Node::NamespaceImport { .. }
557                    )
558                })
559                .collect();
560            for sn in &top_level {
561                self.compile_discarded_stmt(sn)?;
562            }
563            // E4.1 entrypoint convention: a top-level `fn main(harness: Harness)`
564            // is invoked automatically with the runtime-provided root
565            // capability. The typechecker rejects every other signature with
566            // HARN-NAM-101 so we don't need to re-validate the shape here.
567            if Self::has_top_level_fn_main(program) {
568                self.chunk.emit(Op::RootHarness, self.line);
569                self.emit_named_call("main", 1);
570                pipeline_emits_value = true;
571            }
572        }
573
574        self.drain_finallys_to_floor(0)?;
575        if !pipeline_emits_value {
576            self.chunk.emit(Op::Nil, self.line);
577        }
578        self.chunk.emit(Op::Return, self.line);
579        super::ensure_chunk_addressable(&self.chunk, "the program body", self.line)?;
580        Ok(self.chunk)
581    }
582
583    /// True when the program declares a top-level `fn main(...)`. Drives the
584    /// auto-call wired by `compile()` for the new `main(harness: Harness)`
585    /// entrypoint convention.
586    fn has_top_level_fn_main(program: &[SNode]) -> bool {
587        program
588            .iter()
589            .any(|sn| matches!(peel_node(sn), Node::FnDecl { name, .. } if name == "main"))
590    }
591
592    /// Compile a specific named pipeline (for test runners).
593    pub fn compile_named(
594        self,
595        program: &[SNode],
596        pipeline_name: &str,
597    ) -> Result<Chunk, CompileError> {
598        self.compile_named_inner(program, pipeline_name)
599    }
600
601    fn compile_named_inner(
602        mut self,
603        program: &[SNode],
604        pipeline_name: &str,
605    ) -> Result<Chunk, CompileError> {
606        self.prepare_module_context(program);
607
608        for sn in program {
609            if matches!(
610                &sn.node,
611                Node::ImportDecl { .. }
612                    | Node::SelectiveImport { .. }
613                    | Node::NamespaceImport { .. }
614            ) {
615                self.compile_node(sn)?;
616            }
617        }
618        let target = program.iter().find(
619            |sn| matches!(peel_node(sn), Node::Pipeline { name, .. } if name == pipeline_name),
620        );
621
622        if let Some(sn) = target {
623            self.compile_top_level_declarations(program)?;
624            if let Node::Pipeline {
625                body,
626                extends,
627                params,
628                throws,
629                ..
630            } = peel_node(sn)
631            {
632                self.compile_with_pipeline_captures(
633                    program,
634                    body,
635                    extends.as_deref(),
636                    |compiler| {
637                        let saved = std::mem::replace(&mut compiler.module_level, false);
638                        let saved_throw =
639                            std::mem::replace(&mut compiler.declared_throw, throws.is_some());
640                        if let Some(harness) = params.first().filter(|param| {
641                            matches!(
642                                param.type_expr.as_ref(),
643                                Some(TypeExpr::Named(name)) if name == "Harness"
644                            )
645                        }) {
646                            compiler.chunk.emit(Op::RootHarness, compiler.line);
647                            compiler.emit_define_binding(&harness.name, false);
648                        }
649                        if let Some(parent_name) = extends {
650                            compiler.compile_parent_pipeline(program, parent_name)?;
651                        }
652                        let result = compiler.compile_block(body);
653                        compiler.module_level = saved;
654                        compiler.declared_throw = saved_throw;
655                        result
656                    },
657                )?;
658            }
659        }
660
661        self.drain_finallys_to_floor(0)?;
662        self.chunk.emit(Op::Nil, self.line);
663        self.chunk.emit(Op::Return, self.line);
664        super::ensure_chunk_addressable(&self.chunk, "the pipeline body", self.line)?;
665        Ok(self.chunk)
666    }
667
668    /// Emit bytecode preamble for default parameter values.
669    /// For each param with a default at index i, emits:
670    ///   GetArgc; PushInt (i+1); GreaterEqual; JumpIfTrue <skip>;
671    ///   [compile default expr]; DefLet param_name; <skip>:
672    pub(super) fn emit_default_preamble(
673        &mut self,
674        params: &[TypedParam],
675    ) -> Result<(), CompileError> {
676        for (i, param) in params.iter().enumerate() {
677            if let Some(default_expr) = &param.default_value {
678                self.chunk.emit(Op::GetArgc, self.line);
679                let threshold_idx = self.chunk.add_constant(Constant::Int((i + 1) as i64));
680                self.chunk.emit_u16(Op::Constant, threshold_idx, self.line);
681                self.chunk.emit(Op::GreaterEqual, self.line);
682                let skip_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
683                // JumpIfTrue doesn't pop its boolean operand.
684                self.chunk.emit(Op::Pop, self.line);
685                // Compile the default with this param and all *later* params
686                // hidden from local resolution. A default is evaluated left to
687                // right at call time: it may reference an earlier parameter,
688                // but a mention of its own name (or a later, not-yet-bound
689                // parameter) must resolve to the enclosing scope — e.g.
690                // `let n = 7; fn f(n = n * 2)` reads the outer `n`. Without the
691                // mask, `n` bound to the param's own unset slot and threw at
692                // runtime. Earlier params stay visible.
693                let masked = self.mask_param_names(&params[i..]);
694                let result = self.compile_node(default_expr);
695                self.restore_param_names(masked);
696                result?;
697                self.emit_init_or_define_binding(&param.name, false);
698                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
699                self.chunk.patch_jump(skip_jump);
700                self.chunk.emit(Op::Pop, self.line);
701                self.chunk.patch_jump(end_jump);
702            }
703        }
704        Ok(())
705    }
706
707    /// Emit body-local type checks that call-site validation cannot cover.
708    /// Ordinary supplied arguments are validated by precomputed
709    /// [`crate::chunk::ParamSlot`] guards before the frame is entered. The
710    /// bytecode preamble still checks interface parameters, because interface
711    /// satisfaction depends on compiler-collected method metadata, and checks
712    /// defaulted schema parameters only when the caller omitted that argument.
713    pub(super) fn emit_type_checks(&mut self, params: &[TypedParam]) {
714        for (param_index, param) in params.iter().enumerate() {
715            if let Some(type_expr) = &param.type_expr {
716                let check_type = if param.rest {
717                    harn_parser::TypeExpr::List(Box::new(type_expr.clone()))
718                } else {
719                    type_expr.clone()
720                };
721
722                if let harn_parser::TypeExpr::Named(name) = &check_type {
723                    if let Some(methods) = self.interface_methods.get(name).cloned() {
724                        let fn_idx = self.string_constant("__assert_interface");
725                        self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
726                        self.emit_get_binding(&param.name);
727                        let name_idx = self.string_constant(&param.name);
728                        self.chunk.emit_u16(Op::Constant, name_idx, self.line);
729                        let iface_idx = self.string_constant(name);
730                        self.chunk.emit_u16(Op::Constant, iface_idx, self.line);
731                        let methods_str = methods.join(",");
732                        let methods_idx = self.owned_string_constant(methods_str);
733                        self.chunk.emit_u16(Op::Constant, methods_idx, self.line);
734                        self.chunk.emit_u8(Op::Call, 4, self.line);
735                        self.chunk.emit(Op::Pop, self.line);
736                        continue;
737                    }
738                }
739
740                if param.default_value.is_some() {
741                    if let Some(schema) = Self::type_expr_to_schema_value(&check_type) {
742                        self.emit_default_param_schema_check(param_index, param, &schema);
743                    }
744                }
745            }
746        }
747    }
748
749    fn emit_default_param_schema_check(
750        &mut self,
751        param_index: usize,
752        param: &TypedParam,
753        schema: &VmValue,
754    ) {
755        self.chunk.emit(Op::GetArgc, self.line);
756        let threshold_idx = self
757            .chunk
758            .add_constant(Constant::Int((param_index + 1) as i64));
759        self.chunk.emit_u16(Op::Constant, threshold_idx, self.line);
760        self.chunk.emit(Op::GreaterEqual, self.line);
761        let supplied_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
762        self.chunk.emit(Op::Pop, self.line);
763        self.emit_schema_assert_call(param, schema);
764        let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
765        self.chunk.patch_jump(supplied_jump);
766        self.chunk.emit(Op::Pop, self.line);
767        self.chunk.patch_jump(end_jump);
768    }
769
770    fn emit_schema_assert_call(&mut self, param: &TypedParam, schema: &VmValue) {
771        let fn_idx = self.string_constant("__assert_schema");
772        self.chunk.emit_u16(Op::Constant, fn_idx, self.line);
773        self.emit_get_binding(&param.name);
774        let name_idx = self.string_constant(&param.name);
775        self.chunk.emit_u16(Op::Constant, name_idx, self.line);
776        self.emit_vm_value_literal(schema);
777        self.chunk.emit_u8(Op::Call, 3, self.line);
778        self.chunk.emit(Op::Pop, self.line);
779    }
780
781    #[doc(hidden)]
782    pub fn type_expr_to_schema_value(type_expr: &harn_parser::TypeExpr) -> Option<VmValue> {
783        match type_expr {
784            harn_parser::TypeExpr::Named(name) => match name.as_str() {
785                "any" | "unknown" => Some(VmValue::dict(BTreeMap::<String, VmValue>::new())),
786                "int" | "float" | "string" | "bool" | "list" | "dict" | "set" | "nil"
787                | "closure" | "bytes" => Some(VmValue::dict(BTreeMap::from([(
788                    "type".to_string(),
789                    VmValue::String(arcstr::ArcStr::from(name.as_str())),
790                )]))),
791                _ => None,
792            },
793            harn_parser::TypeExpr::Shape(fields) => {
794                let mut properties = BTreeMap::new();
795                let mut required = Vec::new();
796                for field in fields {
797                    let mut field_schema = Self::type_expr_to_schema_value(&field.type_expr)?;
798                    if field.optional {
799                        field_schema = VmValue::dict(BTreeMap::from([(
800                            "union".to_string(),
801                            VmValue::List(std::sync::Arc::new(vec![
802                                field_schema,
803                                VmValue::dict(BTreeMap::from([(
804                                    "type".to_string(),
805                                    VmValue::String(arcstr::ArcStr::from("nil")),
806                                )])),
807                            ])),
808                        )]));
809                    }
810                    properties.insert(field.name.clone(), field_schema);
811                    if !field.optional {
812                        required.push(VmValue::String(arcstr::ArcStr::from(field.name.as_str())));
813                    }
814                }
815                let mut out = BTreeMap::new();
816                out.put_str("type", "dict");
817                out.insert("properties".to_string(), VmValue::dict(properties));
818                if !required.is_empty() {
819                    out.insert(
820                        "required".to_string(),
821                        VmValue::List(std::sync::Arc::new(required)),
822                    );
823                }
824                Some(VmValue::dict(out))
825            }
826            harn_parser::TypeExpr::OpenShape { .. } => None,
827            harn_parser::TypeExpr::List(inner) => {
828                let mut out = BTreeMap::new();
829                out.put_str("type", "list");
830                let item_schema = Self::type_expr_to_schema_value(inner)?;
831                out.insert("items".to_string(), item_schema);
832                Some(VmValue::dict(out))
833            }
834            // The canonical Harn schema vocabulary currently has homogeneous
835            // `items` but no positional-items contract. Returning `None`
836            // deliberately selects the compiled TypeExpr runtime guard, which
837            // preserves exact arity and slot types instead of weakening a
838            // tuple to a homogeneous list schema.
839            harn_parser::TypeExpr::Tuple(_) => None,
840            harn_parser::TypeExpr::DictType(key, value) => {
841                let mut out = BTreeMap::new();
842                out.put_str("type", "dict");
843                if matches!(key.as_ref(), harn_parser::TypeExpr::Named(name) if name == "string") {
844                    let value_schema = Self::type_expr_to_schema_value(value)?;
845                    out.insert("additional_properties".to_string(), value_schema);
846                }
847                Some(VmValue::dict(out))
848            }
849            harn_parser::TypeExpr::Union(members) => {
850                // Special-case unions of literals: emit as `enum: [...]`
851                // so the schema round-trips as canonical JSON Schema and
852                // is ACP-/OpenAPI-compatible. Mixed unions fall back to
853                // the `union:` key that validators recognize.
854                if !members.is_empty()
855                    && members
856                        .iter()
857                        .all(|m| matches!(m, harn_parser::TypeExpr::LitString(_)))
858                {
859                    let values = members
860                        .iter()
861                        .map(|m| match m {
862                            harn_parser::TypeExpr::LitString(s) => {
863                                VmValue::String(arcstr::ArcStr::from(s.as_str()))
864                            }
865                            _ => unreachable!(),
866                        })
867                        .collect::<Vec<_>>();
868                    return Some(VmValue::dict(BTreeMap::from([
869                        (
870                            "type".to_string(),
871                            VmValue::String(arcstr::ArcStr::from("string")),
872                        ),
873                        (
874                            "enum".to_string(),
875                            VmValue::List(std::sync::Arc::new(values)),
876                        ),
877                    ])));
878                }
879                if !members.is_empty()
880                    && members
881                        .iter()
882                        .all(|m| matches!(m, harn_parser::TypeExpr::LitInt(_)))
883                {
884                    let values = members
885                        .iter()
886                        .map(|m| match m {
887                            harn_parser::TypeExpr::LitInt(v) => VmValue::Int(*v),
888                            _ => unreachable!(),
889                        })
890                        .collect::<Vec<_>>();
891                    return Some(VmValue::dict(BTreeMap::from([
892                        (
893                            "type".to_string(),
894                            VmValue::String(arcstr::ArcStr::from("int")),
895                        ),
896                        (
897                            "enum".to_string(),
898                            VmValue::List(std::sync::Arc::new(values)),
899                        ),
900                    ])));
901                }
902                let branches = members
903                    .iter()
904                    .map(Self::type_expr_to_schema_value)
905                    .collect::<Option<Vec<_>>>()?;
906                if branches.is_empty() {
907                    None
908                } else {
909                    Some(VmValue::dict(BTreeMap::from([(
910                        "union".to_string(),
911                        VmValue::List(std::sync::Arc::new(branches)),
912                    )])))
913                }
914            }
915            harn_parser::TypeExpr::Intersection(members) => {
916                // Encode `A & B` as JSON-Schema `allOf` (the runtime
917                // accepts the snake_case `all_of` key directly). The
918                // value must validate against every branch.
919                let branches = members
920                    .iter()
921                    .map(Self::type_expr_to_schema_value)
922                    .collect::<Option<Vec<_>>>()?;
923                if branches.is_empty() {
924                    None
925                } else {
926                    Some(VmValue::dict(BTreeMap::from([(
927                        "all_of".to_string(),
928                        VmValue::List(std::sync::Arc::new(branches)),
929                    )])))
930                }
931            }
932            harn_parser::TypeExpr::FnType { .. } => Some(VmValue::dict(BTreeMap::from([(
933                "type".to_string(),
934                VmValue::String(arcstr::ArcStr::from("closure")),
935            )]))),
936            harn_parser::TypeExpr::Applied { .. } => None,
937            harn_parser::TypeExpr::Iter(_)
938            | harn_parser::TypeExpr::Generator(_)
939            | harn_parser::TypeExpr::Stream(_) => None,
940            harn_parser::TypeExpr::Never => None,
941            harn_parser::TypeExpr::LitString(s) => Some(VmValue::dict(BTreeMap::from([
942                (
943                    "type".to_string(),
944                    VmValue::String(arcstr::ArcStr::from("string")),
945                ),
946                (
947                    "const".to_string(),
948                    VmValue::String(arcstr::ArcStr::from(s.as_str())),
949                ),
950            ]))),
951            harn_parser::TypeExpr::LitInt(v) => Some(VmValue::dict(BTreeMap::from([
952                (
953                    "type".to_string(),
954                    VmValue::String(arcstr::ArcStr::from("int")),
955                ),
956                ("const".to_string(), VmValue::Int(*v)),
957            ]))),
958            harn_parser::TypeExpr::Owned(inner) => Self::type_expr_to_schema_value(inner),
959        }
960    }
961
962    pub(super) fn emit_vm_value_literal(&mut self, value: &VmValue) {
963        match value {
964            VmValue::String(text) => {
965                let idx = self.string_constant(text);
966                self.chunk.emit_u16(Op::Constant, idx, self.line);
967            }
968            VmValue::Int(number) => {
969                let idx = self.chunk.add_constant(Constant::Int(*number));
970                self.chunk.emit_u16(Op::Constant, idx, self.line);
971            }
972            VmValue::Float(number) => {
973                let idx = self.chunk.add_constant(Constant::Float(*number));
974                self.chunk.emit_u16(Op::Constant, idx, self.line);
975            }
976            VmValue::Bool(value) => {
977                let idx = self.chunk.add_constant(Constant::Bool(*value));
978                self.chunk.emit_u16(Op::Constant, idx, self.line);
979            }
980            VmValue::Nil => self.chunk.emit(Op::Nil, self.line),
981            VmValue::List(items) => {
982                for item in items.iter() {
983                    self.emit_vm_value_literal(item);
984                }
985                self.chunk
986                    .emit_u16(Op::BuildList, items.len() as u16, self.line);
987            }
988            VmValue::Dict(entries) => {
989                for (key, item) in entries.iter() {
990                    let key_idx = self.string_constant(key);
991                    self.chunk.emit_u16(Op::Constant, key_idx, self.line);
992                    self.emit_vm_value_literal(item);
993                }
994                self.chunk
995                    .emit_u16(Op::BuildDict, entries.len() as u16, self.line);
996            }
997            _ => {}
998        }
999    }
1000
1001    /// Emit the extra u16 type name index after a TryCatchSetup jump.
1002    pub(super) fn emit_type_name_extra(&mut self, type_name_idx: u16) {
1003        let hi = (type_name_idx >> 8) as u8;
1004        let lo = type_name_idx as u8;
1005        self.chunk.code.push(hi);
1006        self.chunk.code.push(lo);
1007        self.chunk.lines.push(self.line);
1008        self.chunk.columns.push(self.column);
1009        self.chunk.lines.push(self.line);
1010        self.chunk.columns.push(self.column);
1011    }
1012
1013    /// Compile a try/catch body block (produces a value on the stack).
1014    pub(super) fn compile_try_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
1015        if body.is_empty() {
1016            self.chunk.emit(Op::Nil, self.line);
1017        } else {
1018            self.compile_scoped_block(body)?;
1019        }
1020        Ok(())
1021    }
1022
1023    /// Compile catch error binding (error value is on stack from handler).
1024    pub(super) fn compile_catch_binding(
1025        &mut self,
1026        error_var: &Option<String>,
1027    ) -> Result<(), CompileError> {
1028        if let Some(var_name) = error_var {
1029            self.emit_define_binding(var_name, false);
1030        } else {
1031            self.chunk.emit(Op::Pop, self.line);
1032        }
1033        Ok(())
1034    }
1035
1036    /// True if there are any pending cleanup bodies.
1037    pub(super) fn has_pending_finally(&self) -> bool {
1038        !self.finally_bodies.is_empty()
1039    }
1040
1041    /// Register a cleanup body for the region that follows and install the
1042    /// exception handler that runs it when an error leaves that region.
1043    pub(super) fn push_cleanup(&mut self, body: Vec<SNode>) {
1044        let handler_depth = self.handler_depth;
1045        self.handler_depth += 1;
1046        let error_jump = self.chunk.emit_jump(Op::TryCatchPreserve, self.line);
1047        let empty_type = self.string_constant("");
1048        self.emit_type_name_extra(empty_type);
1049        self.finally_bodies.push(FinallyEntry {
1050            body,
1051            handler_depth,
1052            declared_throw: self.declared_throw,
1053            error_jump,
1054        });
1055    }
1056
1057    /// Close the innermost cleanup region: on the normal path pop its handler
1058    /// and run the body; on the exception path run the body and rethrow.
1059    ///
1060    /// The entry is removed before either copy of the body is compiled, so a
1061    /// `return`/`break`/`continue` inside the body runs only the cleanups
1062    /// outside it. The runtime pops the handler before delivering an error,
1063    /// so a throw from the body replaces the original error and escapes.
1064    fn pop_cleanup(&mut self) -> Result<(), CompileError> {
1065        let entry = self.finally_bodies.pop().expect("pending cleanup");
1066        debug_assert_eq!(self.handler_depth, entry.handler_depth + 1);
1067        self.handler_depth = entry.handler_depth;
1068        self.chunk.emit(Op::PopHandler, self.line);
1069        self.compile_finally_inline(&entry.body, entry.declared_throw)?;
1070        let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
1071
1072        self.chunk.patch_jump(entry.error_jump);
1073        self.temp_counter += 1;
1074        let temp_name = format!("__finally_err_{}__", self.temp_counter);
1075        self.emit_define_binding(&temp_name, true);
1076        self.compile_finally_inline(&entry.body, entry.declared_throw)?;
1077        self.emit_get_binding(&temp_name);
1078        self.chunk.emit(Op::Rethrow, self.line);
1079
1080        self.chunk.patch_jump(end_jump);
1081        Ok(())
1082    }
1083
1084    pub(super) fn declare_param_slots(&mut self, params: &[TypedParam]) {
1085        for param in params {
1086            self.define_local_slot(&param.name, false);
1087        }
1088    }
1089
1090    /// Temporarily remove the given parameters' names from the innermost local
1091    /// scope so that, while compiling a default-value expression, references to
1092    /// them resolve to the enclosing scope instead of their not-yet-bound param
1093    /// slots. Returns the removed bindings so [`Self::restore_param_names`] can
1094    /// reinstate them afterward. See [`Self::emit_default_preamble`].
1095    pub(super) fn mask_param_names(
1096        &mut self,
1097        params: &[TypedParam],
1098    ) -> Vec<(String, super::LocalBinding)> {
1099        let mut removed = Vec::new();
1100        if let Some(scope) = self.local_scopes.last_mut() {
1101            for param in params {
1102                if let Some(binding) = scope.remove(&param.name) {
1103                    removed.push((param.name.clone(), binding));
1104                }
1105            }
1106        }
1107        removed
1108    }
1109
1110    /// Reinstate parameter names removed by [`Self::mask_param_names`].
1111    pub(super) fn restore_param_names(&mut self, removed: Vec<(String, super::LocalBinding)>) {
1112        if let Some(scope) = self.local_scopes.last_mut() {
1113            for (name, binding) in removed {
1114                scope.insert(name, binding);
1115            }
1116        }
1117    }
1118
1119    /// Seed exact source bindings captured by nested callables in the body
1120    /// about to be compiled. Parser-owned lexical analysis accounts for
1121    /// parameters, patterns, blocks, loops, catches, selects, and nested
1122    /// callable boundaries before the VM decides whether to use `DefCell`.
1123    pub(super) fn seed_captured_idents(&mut self, body: &[SNode]) {
1124        let match_patterns = self.lexical_match_pattern_catalog();
1125        self.captured_bindings =
1126            harn_parser::lexical::captured_bindings_in_nested_callables(body, &match_patterns);
1127    }
1128
1129    fn seed_module_captured_idents(&mut self, body: &[SNode]) {
1130        let match_patterns = self.lexical_match_pattern_catalog();
1131        self.captured_bindings =
1132            harn_parser::lexical::captured_bindings_in_compiled_module(body, &match_patterns);
1133    }
1134
1135    pub(super) fn lexical_match_pattern_catalog(
1136        &self,
1137    ) -> harn_parser::lexical::MatchPatternCatalog {
1138        if self.imported_enum_candidates.is_empty() {
1139            return harn_parser::lexical::MatchPatternCatalog::new(
1140                &self.enum_names,
1141                &self.enum_variant_owners,
1142            );
1143        }
1144        let mut enum_names = self.enum_names.clone();
1145        enum_names.extend(self.imported_enum_candidates.iter().cloned());
1146        harn_parser::lexical::MatchPatternCatalog::new(&enum_names, &self.enum_variant_owners)
1147    }
1148
1149    pub(super) fn begin_scope(&mut self) {
1150        self.chunk.emit(Op::PushScope, self.line);
1151        self.scope_depth += 1;
1152        let enum_catalog = self.enum_catalog_snapshot();
1153        self.enum_catalog_scopes.push(enum_catalog);
1154        self.type_scopes.push(std::collections::HashMap::new());
1155        self.local_scopes.push(std::collections::HashMap::new());
1156    }
1157
1158    pub(super) fn end_scope(&mut self) {
1159        if self.scope_depth > 0 {
1160            self.chunk.emit(Op::PopScope, self.line);
1161            self.scope_depth -= 1;
1162            if let Some(snapshot) = self.enum_catalog_scopes.pop() {
1163                self.restore_enum_catalog(snapshot);
1164            }
1165            self.type_scopes.pop();
1166            self.local_scopes.pop();
1167        }
1168    }
1169
1170    /// Emit cleanup for an abrupt control-flow path without changing the
1171    /// compiler's lexical scope stacks for the source path that follows it.
1172    pub(super) fn emit_scope_unwind_to(&mut self, target_depth: usize) {
1173        for _ in target_depth..self.scope_depth {
1174            self.chunk.emit(Op::PopScope, self.line);
1175        }
1176    }
1177
1178    pub(super) fn compile_scoped_block(&mut self, stmts: &[SNode]) -> Result<(), CompileError> {
1179        self.begin_scope();
1180        let finally_floor = self.finally_bodies.len();
1181        if stmts.is_empty() {
1182            self.chunk.emit(Op::Nil, self.line);
1183        } else {
1184            self.compile_block(stmts)?;
1185        }
1186        self.drain_finallys_to_floor(finally_floor)?;
1187        self.end_scope();
1188        Ok(())
1189    }
1190
1191    pub(super) fn compile_scoped_statements(
1192        &mut self,
1193        stmts: &[SNode],
1194    ) -> Result<(), CompileError> {
1195        self.begin_scope();
1196        self.record_monomorphic_var_bindings(stmts);
1197        let finally_floor = self.finally_bodies.len();
1198        for sn in stmts {
1199            self.compile_discarded_stmt(sn)?;
1200        }
1201        self.drain_finallys_to_floor(finally_floor)?;
1202        self.end_scope();
1203        Ok(())
1204    }
1205
1206    /// Close pending cleanup regions down to a saved floor in LIFO order.
1207    /// See [`Self::pop_cleanup`].
1208    pub(super) fn drain_finallys_to_floor(&mut self, floor: usize) -> Result<(), CompileError> {
1209        while self.finally_bodies.len() > floor {
1210            self.pop_cleanup()?;
1211        }
1212        Ok(())
1213    }
1214
1215    /// Run the pending cleanup bodies a non-local transfer (`return`,
1216    /// `break`, `continue`) crosses on its way down to `floor`, innermost
1217    /// first, then restore the pending stack.
1218    ///
1219    /// Before each body runs, the handlers above it, including its own, are
1220    /// popped, so a throw from the body reaches only the handlers outside it.
1221    /// `handler_floor` is the handler depth the transfer lands at; `None`
1222    /// leaves the remaining handlers to the frame teardown of a `return`.
1223    ///
1224    /// Each body is removed from the stack *before* it is inlined, so a
1225    /// `return`/`break`/`continue` inside a finally body runs only the
1226    /// finallys *outside* it instead of re-running the one it is in — which
1227    /// otherwise recursed forever at compile time and aborted the process
1228    /// with a stack overflow. The stack is restored afterward because a
1229    /// transfer is a branch: the code the compiler emits after it still needs
1230    /// the pending cleanups for the fall-through and sibling paths.
1231    pub(super) fn run_pending_finallys_for_transfer(
1232        &mut self,
1233        floor: usize,
1234        handler_floor: Option<usize>,
1235    ) -> Result<(), CompileError> {
1236        let saved_entries = self.finally_bodies[floor..].to_vec();
1237        let saved_handler_depth = self.handler_depth;
1238        let result = self.unwind_for_transfer(floor, handler_floor);
1239        self.finally_bodies.truncate(floor);
1240        self.finally_bodies.extend(saved_entries);
1241        self.handler_depth = saved_handler_depth;
1242        result
1243    }
1244
1245    fn unwind_for_transfer(
1246        &mut self,
1247        floor: usize,
1248        handler_floor: Option<usize>,
1249    ) -> Result<(), CompileError> {
1250        while self.finally_bodies.len() > floor {
1251            let entry = self.finally_bodies.pop().expect("non-empty by guard");
1252            self.emit_pop_handlers_to(entry.handler_depth);
1253            self.compile_finally_inline(&entry.body, entry.declared_throw)?;
1254        }
1255        if let Some(handler_floor) = handler_floor {
1256            self.emit_pop_handlers_to(handler_floor);
1257        }
1258        Ok(())
1259    }
1260
1261    fn emit_pop_handlers_to(&mut self, depth: usize) {
1262        while self.handler_depth > depth {
1263            self.chunk.emit(Op::PopHandler, self.line);
1264            self.handler_depth -= 1;
1265        }
1266    }
1267
1268    /// Register an auto-drop defer for an `owned<T>` binding. The drop runs
1269    /// at scope exit alongside any user-written `defer { ... }` blocks (LIFO
1270    /// order) and on `return` / `break` / `continue` / `throw` via the
1271    /// existing finally-unwinding machinery.
1272    pub(super) fn maybe_register_owned_drop(
1273        &mut self,
1274        pattern: &harn_parser::BindingPattern,
1275        type_ann: Option<&TypeExpr>,
1276        span: harn_lexer::Span,
1277    ) {
1278        // Auto-drop only fires when the user explicitly opted in via
1279        // `owned<T>` on a single-identifier binding. Destructured patterns
1280        // (`{a, b}`, `[a, b]`, pairs) aren't auto-dropped: ownership of a
1281        // composite isn't well-defined, and users can wrap individual fields
1282        // with `owned<T>` and bind them separately if needed.
1283        let Some(ty) = type_ann else {
1284            return;
1285        };
1286        if !matches!(ty, TypeExpr::Owned(_)) {
1287            return;
1288        }
1289        let harn_parser::BindingPattern::Identifier(name) = pattern else {
1290            return;
1291        };
1292        if harn_parser::is_discard_name(name) {
1293            return;
1294        }
1295        let call = harn_parser::spanned(
1296            Node::FunctionCall {
1297                name: "drop".to_string(),
1298                args: vec![harn_parser::spanned(Node::Identifier(name.clone()), span)],
1299                type_args: Vec::new(),
1300            },
1301            span,
1302        );
1303        self.push_cleanup(vec![call]);
1304    }
1305
1306    /// Compile a statement that appears in a value-discarding sequence —
1307    /// the script-mode module body, an inherited pipeline body, and block
1308    /// interiors — then pop its value when `produces_value` says it left
1309    /// one.
1310    ///
1311    /// In debug builds this also asserts the operand stack stayed balanced
1312    /// across the statement: a straight-line statement must net exactly one
1313    /// value when `produces_value` is true and zero otherwise. That turns a
1314    /// `produces_value` misclassification — like the attributed-decl gap
1315    /// fixed in #2610, where the loop popped against an empty stack — from a
1316    /// latent runtime "Stack underflow" (often masked further by the
1317    /// bytecode cache, #2621) into a loud compile-time failure in tests/CI.
1318    /// Statements containing branches or other non-linearly-modeled opcodes
1319    /// can't be summed by the lightweight model, so the assertion skips them
1320    /// (see [`Chunk::balance_delta_since`]).
1321    pub(super) fn compile_discarded_stmt(&mut self, sn: &SNode) -> Result<(), CompileError> {
1322        #[cfg(debug_assertions)]
1323        let probe = self.chunk.balance_probe();
1324        self.compile_node(sn)?;
1325        #[allow(unused_mut)]
1326        let mut produces = Self::produces_value(&sn.node);
1327        // Test-only hook: deliberately miswire the classification to prove
1328        // the balance assertion below trips on a `produces_value` gap (the
1329        // #2622 verification). No-op in non-test builds.
1330        #[cfg(test)]
1331        if let Some(forced) = FORCE_DISCARDED_PRODUCES_VALUE.with(std::cell::Cell::get) {
1332            produces = forced;
1333        }
1334        #[cfg(debug_assertions)]
1335        if let Some(delta) = self.chunk.balance_delta_since(probe) {
1336            let expected = i32::from(produces);
1337            debug_assert_eq!(
1338                delta, expected,
1339                "operand-stack imbalance at line {}: produces_value={produces} but the \
1340                 node's emitted bytecode netted {delta} (expected {expected}). A \
1341                 `produces_value` arm is out of sync with this node's codegen — see #2622.\n\
1342                 node: {:?}",
1343                self.line, sn.node,
1344            );
1345        }
1346        if produces {
1347            self.chunk.emit(Op::Pop, self.line);
1348        }
1349        Ok(())
1350    }
1351
1352    pub(super) fn compile_block(&mut self, stmts: &[SNode]) -> Result<(), CompileError> {
1353        self.record_monomorphic_var_bindings(stmts);
1354        let callable_declarations = stmts
1355            .iter()
1356            .enumerate()
1357            .filter_map(|(index, node)| {
1358                harn_parser::lexical::hoisted_callable_name(node)
1359                    .map(|name| (index, name.to_string()))
1360            })
1361            .collect::<Vec<_>>();
1362        let callable_names = callable_declarations
1363            .iter()
1364            .map(|(_, name)| name.as_str())
1365            .collect::<std::collections::HashSet<_>>();
1366        let mut emitted_callables = std::collections::HashSet::new();
1367
1368        for (i, snode) in stmts.iter().enumerate() {
1369            if harn_parser::lexical::hoisted_callable_name(snode).is_some() {
1370                if emitted_callables.insert(i) {
1371                    self.compile_discarded_stmt(snode)?;
1372                }
1373                if i == stmts.len() - 1 {
1374                    self.chunk.emit(Op::Nil, self.line);
1375                }
1376                continue;
1377            }
1378
1379            // Function-like declarations are visible throughout their block.
1380            // Materialize only the forward declarations reachable from this
1381            // statement (including mutual-recursion dependencies), at the
1382            // latest safe point. Earlier data bindings have therefore run and
1383            // remain available to the closure, while a forward call cannot
1384            // reach an uninitialized callable slot.
1385            let mut pending = Vec::new();
1386            Self::collect_callable_references(snode, &callable_names, &mut pending);
1387            let mut reachable = std::collections::HashSet::new();
1388            while let Some(name) = pending.pop() {
1389                if !reachable.insert(name.clone()) {
1390                    continue;
1391                }
1392                for (index, declaration_name) in &callable_declarations {
1393                    if declaration_name == &name {
1394                        Self::collect_callable_references(
1395                            &stmts[*index],
1396                            &callable_names,
1397                            &mut pending,
1398                        );
1399                    }
1400                }
1401            }
1402            for (index, name) in &callable_declarations {
1403                if reachable.contains(name) && emitted_callables.insert(*index) {
1404                    self.compile_discarded_stmt(&stmts[*index])?;
1405                }
1406            }
1407
1408            if i == stmts.len() - 1 {
1409                // The block's value is its last statement's. Backfill a `Nil`
1410                // when that statement produced none, so the block always
1411                // leaves exactly one value on the stack.
1412                self.compile_node(snode)?;
1413                if !Self::produces_value(&snode.node) {
1414                    self.chunk.emit(Op::Nil, self.line);
1415                }
1416            } else {
1417                self.compile_discarded_stmt(snode)?;
1418            }
1419        }
1420        Ok(())
1421    }
1422
1423    fn collect_callable_references(
1424        node: &SNode,
1425        callable_names: &std::collections::HashSet<&str>,
1426        out: &mut Vec<String>,
1427    ) {
1428        match &node.node {
1429            Node::Identifier(name) | Node::FunctionCall { name, .. }
1430                if callable_names.contains(name.as_str()) =>
1431            {
1432                out.push(name.clone());
1433            }
1434            _ => {}
1435        }
1436        for child in harn_parser::visit::immediate_children(node) {
1437            Self::collect_callable_references(child, callable_names, out);
1438        }
1439    }
1440
1441    /// Compile a match arm body, ensuring it always pushes exactly one value.
1442    pub(super) fn compile_match_body(&mut self, body: &[SNode]) -> Result<(), CompileError> {
1443        self.begin_scope();
1444        let finally_floor = self.finally_bodies.len();
1445        if body.is_empty() {
1446            self.chunk.emit(Op::Nil, self.line);
1447        } else {
1448            self.compile_block(body)?;
1449            if !Self::produces_value(&body.last().unwrap().node) {
1450                self.chunk.emit(Op::Nil, self.line);
1451            }
1452        }
1453        self.drain_finallys_to_floor(finally_floor)?;
1454        self.end_scope();
1455        Ok(())
1456    }
1457
1458    /// Emit the binary op instruction for a compound assignment operator.
1459    pub(super) fn emit_compound_op(&mut self, op: &str) -> Result<(), CompileError> {
1460        match op {
1461            "+" => self.chunk.emit(Op::Add, self.line),
1462            "-" => self.chunk.emit(Op::Sub, self.line),
1463            "*" => self.chunk.emit(Op::Mul, self.line),
1464            "/" => self.chunk.emit(Op::Div, self.line),
1465            "%" => self.chunk.emit(Op::Mod, self.line),
1466            _ => {
1467                return Err(CompileError {
1468                    message: format!("Unknown compound operator: {op}"),
1469                    line: self.line,
1470                })
1471            }
1472        }
1473        Ok(())
1474    }
1475
1476    /// Check if a node produces a value on the stack that needs to be popped.
1477    pub(super) fn produces_value(node: &Node) -> bool {
1478        harn_parser::node_produces_value(node)
1479    }
1480}
1481
1482impl Default for Compiler {
1483    fn default() -> Self {
1484        Self::new()
1485    }
1486}