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