Skip to main content

harn_vm/compiler/
state.rs

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