Skip to main content

harn_kernel/compiler/
state.rs

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