Skip to main content

harn_kernel/compiler/
state.rs

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