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