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