Skip to main content

harn_kernel/compiler/
mod.rs

1use harn_parser::{Node, SNode, TypeExpr, TypeParam, UnaryOperator};
2
3/// One declared struct field retained through compilation for construction
4/// layout and runtime field-type assertions (harn#6268).
5#[derive(Clone, Debug)]
6pub(super) struct StructFieldLayout {
7    pub(super) name: String,
8    pub(super) type_expr: Option<TypeExpr>,
9    pub(super) optional: bool,
10}
11
12impl StructFieldLayout {
13    pub(super) fn from_ast(field: &harn_parser::StructField) -> Self {
14        Self {
15            name: field.name.clone(),
16            type_expr: field.type_expr.clone(),
17            optional: field.optional,
18        }
19    }
20}
21
22mod bindings;
23mod callable_entry;
24mod catalogs;
25mod closures;
26mod concurrency;
27mod decls;
28mod entry;
29mod error;
30mod error_handling;
31mod expressions;
32mod module;
33mod mutating_methods;
34mod optimizer;
35mod patterns;
36mod pipe;
37mod pipelines;
38mod schema_types;
39mod state;
40mod statements;
41#[cfg(test)]
42mod tests;
43mod tool_parameters;
44mod type_facts;
45mod yield_scan;
46
47pub use error::CompileError;
48pub use module::{
49    CompiledPortableModule, PortableExportKind, PortableImport, PortableSourceModule,
50    PortableSourcePackage,
51};
52pub use schema_types::TypeSchemaInitializer;
53
54use crate::chunk::{Chunk, Constant, Op};
55
56/// A compiled top-level callable invocation.
57///
58/// The bootstrap chunk initializes the source module once and yields either
59/// the target callable or `[fixture, target]`. [`crate::Vm`] owns invocation:
60/// it calls the optional fixture, prepends that value to the explicit
61/// arguments, invokes the target through the ordinary callable arity/type
62/// path, and runs the pipeline-finish lifecycle once around the whole entry.
63///
64/// Keeping the bootstrap representation private prevents hosts from learning
65/// compiler bytecode conventions or smuggling arguments through VM globals.
66#[derive(Clone)]
67pub struct CompiledCallableEntry {
68    #[doc(hidden)]
69    pub bootstrap: Chunk,
70    #[doc(hidden)]
71    pub has_fixture: bool,
72    #[doc(hidden)]
73    pub fixture_expects_harness: bool,
74    #[doc(hidden)]
75    pub expects_harness: bool,
76}
77
78/// Ordered results from one shared lowering of a source file's requested
79/// callable entries. A declaration-level failure remains local to its request;
80/// parse/import/top-level failures are returned by the batch operation itself.
81pub struct CompiledCallableBatch {
82    /// Results in the same order as requested pipeline entries.
83    pub pipelines: Vec<Result<CompiledCallableEntry, CompileError>>,
84    /// Results in the same order as requested function entries.
85    pub functions: Vec<Result<CompiledCallableEntry, CompileError>>,
86}
87
88/// Jump operands are 16-bit chunk offsets (`emit_jump`, `patch_jump`,
89/// backward loop jumps), so a chunk whose code grows past `u16::MAX`
90/// bytes would silently truncate jump targets and land somewhere wild at
91/// runtime. Every finalized chunk (the program chunk and each compiled
92/// function's chunk) must pass through this guard so oversized bodies
93/// fail compilation instead of miscompiling.
94pub(crate) fn ensure_chunk_addressable(
95    chunk: &Chunk,
96    what: &str,
97    line: u32,
98) -> Result<(), CompileError> {
99    if chunk.code.len() > u16::MAX as usize {
100        return Err(CompileError {
101            message: format!(
102                "{what} compiled to {} bytes of bytecode, more than the 64 KiB a jump \
103                 operand can address; split it into smaller functions",
104                chunk.code.len()
105            ),
106            line,
107        });
108    }
109    Ok(())
110}
111
112/// Environment variable that disables optional compiler optimizations.
113///
114/// The VM still emits structurally required bytecode, such as parameter
115/// slots, but skips semantic-preserving optimizer passes. This gives tests
116/// and benchmarks a stable optimized-vs-unoptimized comparison switch.
117pub const HARN_DISABLE_OPTIMIZATIONS_ENV: &str = "HARN_DISABLE_OPTIMIZATIONS";
118
119/// Controls semantic-preserving compiler optimizations.
120#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
121enum RuntimeSourceAuthority {
122    #[default]
123    None,
124    EmbeddedStdlib,
125    RuntimeOwned,
126}
127
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
129pub struct CompilerOptions {
130    optimize: bool,
131    privileged_wire_authority: bool,
132    runtime_source_authority: RuntimeSourceAuthority,
133    legacy_ambient_capabilities: bool,
134    defer_builtin_linking: bool,
135}
136
137impl CompilerOptions {
138    pub fn optimized() -> Self {
139        Self {
140            optimize: true,
141            privileged_wire_authority: false,
142            runtime_source_authority: RuntimeSourceAuthority::None,
143            legacy_ambient_capabilities: false,
144            defer_builtin_linking: false,
145        }
146    }
147
148    pub fn without_optimizations() -> Self {
149        Self {
150            optimize: false,
151            privileged_wire_authority: false,
152            runtime_source_authority: RuntimeSourceAuthority::None,
153            legacy_ambient_capabilities: false,
154            defer_builtin_linking: false,
155        }
156    }
157
158    /// Options for a trusted embedder-owned wire module.
159    ///
160    /// This is intentionally not selected from source syntax, paths, or an
161    /// environment variable. Only explicit trusted-embedder compiler entry
162    /// points may grant the authority.
163    #[doc(hidden)]
164    pub fn privileged_wire() -> Self {
165        Self {
166            optimize: true,
167            privileged_wire_authority: true,
168            runtime_source_authority: RuntimeSourceAuthority::None,
169            legacy_ambient_capabilities: false,
170            defer_builtin_linking: false,
171        }
172    }
173
174    /// Options for source embedded or generated by the Harn runtime.
175    ///
176    /// This grants private runtime and Harness-method implementation builtins
177    /// without granting host wire authority. Only Rust-owned source entry
178    /// points may select these options.
179    #[doc(hidden)]
180    pub fn runtime_owned_source() -> Self {
181        let mut options = Self::from_env();
182        options.runtime_source_authority = RuntimeSourceAuthority::RuntimeOwned;
183        options
184    }
185
186    /// Options for Harn's immutable, embedder-owned stdlib sources.
187    ///
188    /// This grants stdlib-private and runtime implementation primitives, but
189    /// not Harness methods or privileged wire calls. Source text and paths
190    /// cannot select the authority; the embedded loader and closed-program
191    /// linker grant it explicitly.
192    #[doc(hidden)]
193    pub fn embedded_stdlib() -> Self {
194        let mut options = Self::from_env();
195        options.runtime_source_authority = RuntimeSourceAuthority::EmbeddedStdlib;
196        options
197    }
198
199    pub fn from_env() -> Self {
200        let mut options = if std::env::var_os(HARN_DISABLE_OPTIMIZATIONS_ENV).is_some() {
201            Self::without_optimizations()
202        } else {
203            Self::optimized()
204        };
205        options.legacy_ambient_capabilities = harn_parser::legacy_ambient_capabilities_enabled();
206        options
207    }
208
209    pub fn optimizations_enabled(self) -> bool {
210        self.optimize
211    }
212
213    /// Options for a portable artifact. The closed runtime linker returns a
214    /// structured unsupported diagnostic only if execution reaches a builtin
215    /// that this kernel cannot execute; the frontend still typechecks it.
216    pub(crate) fn portable_artifact() -> Self {
217        Self {
218            defer_builtin_linking: true,
219            ..Self::optimized()
220        }
221    }
222
223    pub(crate) fn defers_builtin_linking(self) -> bool {
224        self.defer_builtin_linking
225    }
226
227    #[doc(hidden)]
228    pub fn privileged_wire_authority(self) -> bool {
229        self.privileged_wire_authority
230    }
231
232    #[doc(hidden)]
233    pub fn runtime_owned_source_authority(self) -> bool {
234        self.runtime_source_authority == RuntimeSourceAuthority::RuntimeOwned
235    }
236
237    #[doc(hidden)]
238    pub fn runtime_internal_authority(self) -> bool {
239        matches!(
240            self.runtime_source_authority,
241            RuntimeSourceAuthority::EmbeddedStdlib | RuntimeSourceAuthority::RuntimeOwned
242        )
243    }
244
245    #[doc(hidden)]
246    pub fn stdlib_internal_authority(self) -> bool {
247        self.runtime_source_authority == RuntimeSourceAuthority::EmbeddedStdlib
248    }
249
250    #[doc(hidden)]
251    pub fn legacy_ambient_capabilities(self) -> bool {
252        self.legacy_ambient_capabilities
253    }
254
255    #[doc(hidden)]
256    pub fn with_legacy_ambient_capabilities(mut self) -> Self {
257        self.legacy_ambient_capabilities = true;
258        self
259    }
260}
261
262impl Default for CompilerOptions {
263    fn default() -> Self {
264        Self::optimized()
265    }
266}
267
268/// Look through an `AttributedDecl` wrapper to the inner declaration.
269/// `compile_named` / `compile` use this so attributed declarations like
270/// `@test pipeline foo(harness: Harness, ...)` are still discoverable by name.
271fn peel_node(sn: &SNode) -> &Node {
272    match &sn.node {
273        Node::AttributedDecl { inner, .. } => &inner.node,
274        other => other,
275    }
276}
277
278/// A pending cleanup body (`finally`, `defer`, or an `owned<T>` drop) and
279/// the exception handler that guards its region. See
280/// `Compiler::finally_bodies` for the unwind semantics.
281#[derive(Clone, Debug)]
282struct FinallyEntry {
283    body: Vec<SNode>,
284    /// Handler depth outside this cleanup's own handler.
285    handler_depth: usize,
286    /// Whether a source-authored throw belongs to a declared application channel.
287    declared_throw: bool,
288    /// `TryCatchSetup` operand to patch with this cleanup's exception path.
289    error_jump: usize,
290}
291
292#[derive(Clone, Debug)]
293struct TypeAliasDefinition {
294    type_params: Vec<TypeParam>,
295    /// `None` marks a selectively imported name. If typechecking accepted it
296    /// in a type expression, its runtime schema binding is the definition.
297    body: Option<TypeExpr>,
298}
299
300/// Tracks loop context for break/continue compilation.
301struct LoopContext {
302    /// Offset of the loop start (for continue).
303    start_offset: usize,
304    /// Positions of break jumps that need patching to the loop end.
305    break_patches: Vec<usize>,
306    /// True if this is a for-in loop (has an iterator to clean up on break).
307    has_iterator: bool,
308    /// Number of exception handlers active at loop entry.
309    handler_depth: usize,
310    /// Number of pending finally bodies at loop entry.
311    finally_depth: usize,
312    /// Lexical scope depth at loop entry.
313    scope_depth: usize,
314}
315
316#[derive(Clone, Copy, Debug)]
317enum LocalStorage {
318    Slot(u16),
319    /// An environment-backed cell that still participates in lexical
320    /// shadowing. Captured mutable bindings use cells so closures see later
321    /// writes, but a later same-named declaration must not retroactively
322    /// redirect earlier references into a new local slot.
323    Environment,
324}
325
326#[derive(Clone, Copy, Debug, PartialEq, Eq)]
327enum LocalBindingKind {
328    Value,
329    Callable,
330}
331
332#[derive(Clone, Copy, Debug)]
333struct LocalBinding {
334    storage: LocalStorage,
335    kind: LocalBindingKind,
336    mutable: bool,
337}
338
339struct EnumCatalogSnapshot {
340    names: std::collections::HashSet<String>,
341    variant_owners: std::collections::HashMap<String, Vec<String>>,
342}
343
344/// Compiles an AST into bytecode.
345pub struct Compiler {
346    options: CompilerOptions,
347    chunk: Chunk,
348    line: u32,
349    column: u32,
350    /// Track enum type names so PropertyAccess on them can produce EnumVariant.
351    enum_names: std::collections::HashSet<String>,
352    /// Variant name → owning enum names. Lets a bare call-shaped match
353    /// pattern (`Ok(v)`, `Some(x)`) resolve to its enum without
354    /// qualification when the variant name is unambiguous.
355    enum_variant_owners: std::collections::HashMap<String, Vec<String>>,
356    /// Names introduced by selective imports. A qualified match pattern such
357    /// as `ImportedEnum.Ready(value)` is enum-shaped even though the imported
358    /// declaration is not present in this module's AST. Keep these candidates
359    /// separate from local enum declarations so ordinary imported namespace
360    /// calls continue to use their runtime value.
361    imported_enum_candidates: std::collections::HashSet<String>,
362    /// Whether the imported-enum set came from an authoritative module-graph
363    /// projection. Direct `Compiler::new()` callers retain the conservative
364    /// AST fallback; file-backed callers can opt out when the graph found no
365    /// enum exports without paying for another syntax scan.
366    imported_enum_candidates_authoritative: bool,
367    /// Callables supplied by this source module rather than the builtin
368    /// registry. This includes local declarations plus selective and
369    /// module-graph-resolved wildcard imports.
370    ///
371    /// The distinction matters when a source callable deliberately shares a
372    /// name with a privileged wire builtin: lexical/module resolution owns
373    /// the call, so the builtin exposure policy must not capture it merely by
374    /// spelling. Runtime wire authority is enforced independently of names.
375    source_callable_names: std::collections::HashSet<String>,
376    /// Source spans of enums predeclared into the module catalog. Re-visiting
377    /// those AST nodes during bytecode emission must not replace the final
378    /// prepass view with an earlier duplicate declaration.
379    predeclared_enum_declarations: std::collections::HashSet<(usize, usize)>,
380    /// Catalog snapshots paired with lexical bytecode scopes. Enum
381    /// declarations update the active catalog in source order; restoring the
382    /// snapshot on scope exit prevents a block-local enum from leaking into
383    /// later outer match patterns.
384    enum_catalog_scopes: Vec<EnumCatalogSnapshot>,
385    /// Track struct type names to declared field order and types for indexed
386    /// instances and construction-site field assertions (harn#6268).
387    struct_layouts: std::collections::HashMap<String, Vec<StructFieldLayout>>,
388    /// Track interface names → method names for runtime enforcement.
389    interface_methods: std::collections::HashMap<String, Vec<String>>,
390    /// Stack of active loop contexts for break/continue.
391    loop_stack: Vec<LoopContext>,
392    /// Current depth of exception handlers (for cleanup on break/continue).
393    handler_depth: usize,
394    /// Source-authored throws use the enclosing callable's declared channel.
395    declared_throw: bool,
396    /// Stack of pending cleanup bodies, innermost last.
397    ///
398    /// Each entry owns a runtime exception handler installed when the
399    /// cleanup is registered. Any error leaving the region, whether thrown
400    /// inline, raised by a callee, or produced by a failing operation, lands
401    /// in that handler, which runs the body once and rethrows. Normal exit
402    /// and `return`/`break`/`continue` pop the handler and inline the body.
403    finally_bodies: Vec<FinallyEntry>,
404    /// Counter for unique temp variable names.
405    temp_counter: usize,
406    /// Number of lexical block scopes currently active in this compiled frame.
407    scope_depth: usize,
408    /// Top-level and selectively imported type names used to materialize
409    /// schema expressions. Imported names remain runtime references so module
410    /// initialization can compose them after imports are bound.
411    type_aliases: std::collections::HashMap<String, TypeAliasDefinition>,
412    /// Lightweight compiler-side type facts used only for conservative
413    /// bytecode specialization. This mirrors lexical scopes and is separate
414    /// from the parser's diagnostic type checker so compile-only callers keep
415    /// working without a required type-check pass.
416    type_scopes: Vec<std::collections::HashMap<String, TypeExpr>>,
417    /// `(span.start, span.end)` of every mutable binding (`let` / `for`-item)
418    /// proven *monomorphic*: its value keeps a single primitive type across its
419    /// initializer and every reassignment in scope. Only these bindings may
420    /// carry an initializer-inferred primitive type fact into typed-opcode
421    /// specialization (`AddInt`, `LessInt`, …), which hard-errors on a runtime
422    /// operand-type mismatch. A mutable binding that is reassigned through an
423    /// `any`-typed (or otherwise non-matching) value is *not* recorded here, so
424    /// the compiler keeps it on the generic adaptive path that re-checks operand
425    /// shapes at runtime — see [`Compiler::record_monomorphic_var_bindings`].
426    /// Populated per lexical scope before that scope's statements are compiled;
427    /// keyed by byte span because `Span` is not `Hash`.
428    monomorphic_bindings: std::collections::HashSet<(usize, usize)>,
429    /// Current-chunk string constant index. This avoids repeatedly scanning the
430    /// constant pool while compiling name-heavy scripts.
431    string_constants: std::collections::HashMap<String, u16>,
432    /// Lexical bindings for the current compiled frame. Ordinary locals use
433    /// indexed slots; mutable values captured by nested callables retain an
434    /// environment-backed marker so lexical shadowing and dynamic cell access
435    /// agree on the same declaration.
436    local_scopes: Vec<std::collections::HashMap<String, LocalBinding>>,
437    /// True when this compiler is emitting code outside any function-like
438    /// scope (module top-level statements). `try*` is rejected here
439    /// because the rethrow has no enclosing function to live in.
440    /// Pipeline bodies and nested `Compiler::new()` instances (fn,
441    /// closure, tool, etc.) flip this to false before compiling.
442    module_level: bool,
443    /// Source bindings captured by a nested callable in the body this compiler
444    /// emits. Identity includes the declaration span, so a shadowing parameter
445    /// or block-local never boxes an unrelated same-named `let`.
446    captured_bindings: std::collections::HashSet<harn_parser::lexical::BindingId>,
447    /// Conservative projection for each namespace import in the source file.
448    namespace_import_demands: std::collections::BTreeMap<String, harn_parser::NamespaceDemand>,
449}
450
451impl Compiler {
452    /// Compile a single AST node. Most arm bodies live in per-category
453    /// submodules (expressions, statements, closures, decls, patterns,
454    /// error_handling, concurrency); this function is a thin dispatcher.
455    pub(super) fn compile_node(&mut self, snode: &SNode) -> Result<(), CompileError> {
456        self.line = snode.span.line as u32;
457        self.column = snode.span.column as u32;
458        self.chunk.set_column(self.column);
459        if self.options.optimizations_enabled() {
460            if let Some(folded) = optimizer::fold_constant_expr(snode) {
461                if folded.node != snode.node {
462                    return self.compile_node(&folded);
463                }
464            }
465        }
466        match &snode.node {
467            Node::IntLiteral(n) => {
468                let idx = self.chunk.add_constant(Constant::Int(*n));
469                self.chunk.emit_u16(Op::Constant, idx, self.line);
470            }
471            Node::FloatLiteral(n) => {
472                let idx = self.chunk.add_constant(Constant::Float(*n));
473                self.chunk.emit_u16(Op::Constant, idx, self.line);
474            }
475            Node::StringLiteral(s) | Node::RawStringLiteral(s) => {
476                let idx = self.string_constant(s);
477                self.chunk.emit_u16(Op::Constant, idx, self.line);
478            }
479            Node::BoolLiteral(true) => self.chunk.emit(Op::True, self.line),
480            Node::BoolLiteral(false) => self.chunk.emit(Op::False, self.line),
481            Node::NilLiteral => self.chunk.emit(Op::Nil, self.line),
482            Node::DurationLiteral(ms) => {
483                let ms = i64::try_from(*ms).map_err(|_| CompileError {
484                    message: "duration literal is too large".to_string(),
485                    line: self.line,
486                })?;
487                let idx = self.chunk.add_constant(Constant::Duration(ms));
488                self.chunk.emit_u16(Op::Constant, idx, self.line);
489            }
490            Node::Identifier(name) => {
491                if self.emit_schema_for_alias(name) {
492                    return Ok(());
493                }
494                // A type-alias name in value position denotes its runtime
495                // schema. If materialization failed we would otherwise fall
496                // through to a bare variable load and surface a misleading
497                // `Undefined variable` at runtime. Only a locally-defined
498                // alias body can reach here (imported names and
499                // successfully-lowered aliases take the branch above), so name
500                // the alias and the failure at compile time instead.
501                if let Some(alias) = self.type_aliases.get(name) {
502                    if alias.body.is_some() {
503                        return Err(CompileError {
504                            message: format!(
505                                "cannot materialize a runtime schema for type alias `{name}`: it nests a type with no schema representation (for example an unbounded-recursive generic)"
506                            ),
507                            line: self.line,
508                        });
509                    }
510                }
511                self.emit_get_binding(name);
512            }
513            Node::LetBinding {
514                pattern,
515                value,
516                type_ann,
517                ..
518            } => {
519                let binding_type = match type_ann {
520                    Some(type_ann) => Some(type_ann.clone()),
521                    None => self.infer_expr_type(value),
522                };
523                self.compile_node(value)?;
524                self.emit_binding_type_assertion(pattern, type_ann.as_ref());
525                self.compile_destructuring(pattern, true, snode.span)?;
526                // A `let` is reassignable, so its initializer-inferred primitive
527                // type is only safe for typed-opcode specialization when the
528                // binding is provably monomorphic (proven by
529                // `record_monomorphic_var_bindings`, run before this scope's
530                // statements). Otherwise drop the primitive fact so arithmetic
531                // stays on the generic adaptive path, which re-checks operand
532                // shapes at runtime instead of hard-committing to `AddInt` etc.
533                let binding_type = self.gate_mutable_primitive_type(snode.span, binding_type);
534                self.record_binding_type(pattern, binding_type.clone());
535                self.maybe_register_owned_drop(pattern, binding_type.as_ref(), snode.span);
536            }
537            Node::ConstBinding {
538                pattern,
539                value,
540                type_ann,
541                ..
542            } => {
543                // `const` is an immutable binding. When its initializer is in
544                // the pure const-eval subset over a plain identifier, the
545                // typechecker has already folded it; either way the VM
546                // re-evaluates the same expression, producing the folded value
547                // byte-for-byte. Lowered immutable (destructuring allowed).
548                let binding_type = match type_ann {
549                    Some(type_ann) => Some(type_ann.clone()),
550                    None => self.infer_expr_type(value),
551                };
552                self.compile_node(value)?;
553                self.emit_binding_type_assertion(pattern, type_ann.as_ref());
554                self.compile_destructuring(pattern, false, snode.span)?;
555                self.record_binding_type(pattern, binding_type.clone());
556                self.maybe_register_owned_drop(pattern, binding_type.as_ref(), snode.span);
557            }
558            Node::Assignment {
559                target, value, op, ..
560            } => {
561                self.compile_assignment(target, value, op)?;
562            }
563            Node::BinaryOp { op, left, right } => {
564                self.compile_binary_op(*op, left, right)?;
565            }
566            Node::UnaryOp { op, operand } => {
567                self.compile_node(operand)?;
568                match op {
569                    UnaryOperator::Neg => self.chunk.emit(Op::Negate, self.line),
570                    UnaryOperator::Not => self.chunk.emit(Op::Not, self.line),
571                }
572            }
573            Node::NonNullAssert { operand } => {
574                // `expr!` — identity when present, throws when `nil`. Leaves the
575                // (non-nil) value on the stack. `JumpIfFalse` peeks, so the
576                // `is_nil` bool is popped on both paths.
577                self.compile_node(operand)?; // [value]
578                self.chunk.emit(Op::Dup, self.line); // [value, value]
579                self.chunk.emit(Op::Nil, self.line); // [value, value, nil]
580                self.chunk.emit(Op::Equal, self.line); // [value, is_nil]
581                let present_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
582                // nil path: drop the bool, throw a structured message.
583                self.chunk.emit(Op::Pop, self.line); // [value]
584                let idx =
585                    self.string_constant("non-null assertion failed: value was nil (unwrap_nil)");
586                self.chunk.emit_u16(Op::Constant, idx, self.line);
587                self.chunk.emit(Op::Throw, self.line);
588                // present path: drop the bool, leaving the value.
589                self.chunk.patch_jump(present_jump);
590                self.chunk.emit(Op::Pop, self.line); // [value]
591            }
592            Node::Ternary {
593                condition,
594                true_expr,
595                false_expr,
596            } => {
597                self.compile_node(condition)?;
598                let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
599                self.chunk.emit(Op::Pop, self.line);
600                self.compile_node(true_expr)?;
601                let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
602                self.chunk.patch_jump(else_jump);
603                self.chunk.emit(Op::Pop, self.line);
604                self.compile_node(false_expr)?;
605                self.chunk.patch_jump(end_jump);
606            }
607            Node::FunctionCall { name, args, .. } => {
608                self.compile_function_call(name, args)?;
609            }
610            Node::ValueCall { callee, args } => {
611                self.compile_call_expression(callee, args)?;
612            }
613            Node::MethodCall {
614                object,
615                method,
616                args,
617            } => {
618                self.compile_method_call(object, method, args)?;
619            }
620            Node::OptionalMethodCall {
621                object,
622                method,
623                args,
624            } => {
625                self.compile_node(object)?;
626                for arg in args {
627                    self.compile_node(arg)?;
628                }
629                let name_idx = self.string_constant(method);
630                self.chunk
631                    .emit_method_call_opt(name_idx, args.len() as u8, self.line);
632            }
633            Node::PropertyAccess { object, property } => {
634                self.compile_property_access(object, property)?;
635            }
636            Node::OptionalPropertyAccess { object, property } => {
637                self.compile_node(object)?;
638                let idx = self.string_constant(property);
639                self.chunk.emit_u16(Op::GetPropertyOpt, idx, self.line);
640            }
641            Node::SubscriptAccess { object, index } => {
642                self.compile_node(object)?;
643                self.compile_node(index)?;
644                self.chunk.emit(Op::Subscript, self.line);
645            }
646            Node::OptionalSubscriptAccess { object, index } => {
647                self.compile_node(object)?;
648                self.compile_node(index)?;
649                self.chunk.emit(Op::SubscriptOpt, self.line);
650            }
651            Node::SliceAccess { object, start, end } => {
652                self.compile_node(object)?;
653                if let Some(s) = start {
654                    self.compile_node(s)?;
655                } else {
656                    self.chunk.emit(Op::Nil, self.line);
657                }
658                if let Some(e) = end {
659                    self.compile_node(e)?;
660                } else {
661                    self.chunk.emit(Op::Nil, self.line);
662                }
663                self.chunk.emit(Op::Slice, self.line);
664            }
665            Node::IfElse {
666                condition,
667                then_body,
668                else_body,
669                ..
670            } => {
671                self.compile_if_else(condition, then_body, else_body)?;
672            }
673            Node::WhileLoop { condition, body } => {
674                self.compile_while_loop(condition, body)?;
675            }
676            Node::ForIn {
677                pattern,
678                iterable,
679                body,
680            } => {
681                self.compile_for_in(pattern, iterable, body, snode.span)?;
682            }
683            Node::ReturnStmt { value } => {
684                self.compile_return_stmt(value)?;
685            }
686            Node::BreakStmt => {
687                self.compile_break_stmt()?;
688            }
689            Node::ContinueStmt => {
690                self.compile_continue_stmt()?;
691            }
692            Node::ListLiteral(elements) => {
693                self.compile_list_literal(elements)?;
694            }
695            Node::DictLiteral(entries) => {
696                self.compile_dict_literal(entries)?;
697            }
698            Node::InterpolatedString(segments) => {
699                self.compile_interpolated_string(segments)?;
700            }
701            Node::FnDecl {
702                name,
703                type_params,
704                params,
705                body,
706                is_stream,
707                throws,
708                ..
709            } => {
710                self.compile_fn_decl(name, type_params, params, body, *is_stream, throws.as_ref())?;
711            }
712            Node::ToolDecl {
713                name,
714                description,
715                params,
716                return_type,
717                body,
718                throws,
719                ..
720            } => {
721                self.compile_tool_decl(
722                    name,
723                    description,
724                    params,
725                    return_type,
726                    body,
727                    throws.as_ref(),
728                )?;
729            }
730            Node::SkillDecl { name, fields, .. } => {
731                self.compile_skill_decl(name, fields)?;
732            }
733            Node::EvalPackDecl {
734                binding_name,
735                pack_id,
736                fields,
737                body,
738                summarize,
739                ..
740            } => {
741                self.compile_eval_pack_decl(binding_name, pack_id, fields, body, summarize, true)?;
742            }
743            Node::Closure {
744                params,
745                body,
746                throws,
747                ..
748            } => {
749                self.compile_closure(params, body, throws.is_some())?;
750            }
751            Node::ThrowStmt { value } => {
752                self.compile_throw_stmt(value)?;
753            }
754            Node::MatchExpr { value, arms } => {
755                self.compile_match_expr(value, arms)?;
756            }
757            Node::RangeExpr {
758                start,
759                end,
760                inclusive,
761            } => {
762                let name_idx = self.string_constant("__range__");
763                self.chunk.emit_u16(Op::Constant, name_idx, self.line);
764                self.compile_node(start)?;
765                self.compile_node(end)?;
766                if *inclusive {
767                    self.chunk.emit(Op::True, self.line);
768                } else {
769                    self.chunk.emit(Op::False, self.line);
770                }
771                self.chunk.emit_u8(Op::Call, 3, self.line);
772            }
773            Node::GuardStmt {
774                condition,
775                else_body,
776            } => {
777                self.compile_guard_stmt(condition, else_body)?;
778            }
779            Node::RequireStmt { condition, message } => {
780                self.compile_node(condition)?;
781                let ok_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
782                self.chunk.emit(Op::Pop, self.line);
783                if let Some(message) = message {
784                    self.compile_node(message)?;
785                } else {
786                    let idx = self.string_constant("require condition failed");
787                    self.chunk.emit_u16(Op::Constant, idx, self.line);
788                }
789                self.chunk.emit(Op::Throw, self.line);
790                self.chunk.patch_jump(ok_jump);
791                self.chunk.emit(Op::Pop, self.line);
792            }
793            Node::Block(stmts) => {
794                self.compile_scoped_block(stmts)?;
795            }
796            Node::DeadlineBlock { duration, body } => {
797                self.compile_node(duration)?;
798                self.chunk.emit(Op::DeadlineSetup, self.line);
799                self.compile_scoped_block(body)?;
800                self.chunk.emit(Op::DeadlineEnd, self.line);
801            }
802            Node::MutexBlock { key, body } => {
803                self.begin_scope();
804                let finally_floor = self.finally_bodies.len();
805                match key {
806                    // `mutex(resource) { ... }`: evaluate the resource and key
807                    // the lock on its structural value at runtime.
808                    Some(key_expr) => {
809                        self.compile_node(key_expr)?;
810                        self.chunk.emit(Op::SyncMutexEnterKeyed, self.line);
811                    }
812                    // `mutex { ... }`: key on the lexical call-site (computed in
813                    // the VM from the chunk + instruction pointer) so distinct
814                    // blocks don't contend on one global lock.
815                    None => {
816                        self.chunk.emit(Op::SyncMutexEnter, self.line);
817                    }
818                }
819                for sn in body {
820                    self.compile_discarded_stmt(sn)?;
821                }
822                self.drain_finallys_to_floor(finally_floor)?;
823                self.chunk.emit(Op::Nil, self.line);
824                self.end_scope();
825            }
826            Node::ScopeBlock { body } => {
827                // Structured-concurrency nursery. `TaskScopeEnter` pushes a task
828                // scope; tasks spawned inside register to it. `TaskScopeExit`
829                // joins them (propagating the first error, cancelling the rest).
830                // On `throw`/early exit the scope is unwound and its tasks
831                // cancelled by the frame/handler teardown, mirroring
832                // `held_sync_guards`.
833                self.begin_scope();
834                let finally_floor = self.finally_bodies.len();
835                self.chunk.emit(Op::TaskScopeEnter, self.line);
836                for sn in body {
837                    self.compile_discarded_stmt(sn)?;
838                }
839                self.drain_finallys_to_floor(finally_floor)?;
840                self.chunk.emit(Op::TaskScopeExit, self.line);
841                self.chunk.emit(Op::Nil, self.line);
842                self.end_scope();
843            }
844            Node::DeferStmt { body } => {
845                // Register the body to run on return/throw/scope-exit. The
846                // statement only installs the cleanup's exception handler; the
847                // deferred body is inlined later by the finally-draining
848                // machinery. It leaves the operand stack untouched, matching
849                // `produces_value` == false. Emitting a `Nil` here instead
850                // leaked an unpopped slot per execution, which in a loop body
851                // grew the operand stack without bound (surfaced by the
852                // #2622 balance assertion).
853                self.push_cleanup(body.clone());
854            }
855            Node::YieldExpr { value } => {
856                if let Some(val) = value {
857                    self.compile_node(val)?;
858                } else {
859                    self.chunk.emit(Op::Nil, self.line);
860                }
861                self.chunk.emit(Op::Yield, self.line);
862            }
863            Node::EmitExpr { value } => {
864                self.compile_node(value)?;
865                self.chunk.emit(Op::Yield, self.line);
866            }
867            Node::EnumConstruct {
868                enum_name,
869                variant,
870                args,
871            } => {
872                self.compile_enum_construct(enum_name, variant, args)?;
873            }
874            Node::StructConstruct {
875                struct_name,
876                fields,
877            } => {
878                self.compile_struct_construct(struct_name, fields)?;
879            }
880            Node::ImportDecl { path, .. } => {
881                let idx = self.string_constant(path);
882                self.chunk.emit_u16(Op::Import, idx, self.line);
883            }
884            Node::SelectiveImport { names, path, .. } => {
885                let path_idx = self.string_constant(path);
886                let names_str = names.join(",");
887                let names_idx = self.owned_string_constant(names_str);
888                self.chunk.emit_u16_operands(
889                    Op::SelectiveImport,
890                    &[path_idx, names_idx],
891                    self.line,
892                );
893            }
894            Node::NamespaceImport { alias, path, .. } => {
895                let path_idx = self.string_constant(path);
896                let alias_idx = self.string_constant(alias);
897                match self.namespace_import_demands.get(alias) {
898                    Some(harn_parser::NamespaceDemand::Members(members)) => {
899                        let names_idx = self.owned_string_constant(
900                            members.iter().cloned().collect::<Vec<_>>().join(","),
901                        );
902                        self.chunk.emit_u16_operands(
903                            Op::NamespaceImportMembers,
904                            &[path_idx, alias_idx, names_idx],
905                            self.line,
906                        );
907                    }
908                    Some(harn_parser::NamespaceDemand::Whole) | None => {
909                        self.chunk.emit_u16_operands(
910                            Op::NamespaceImport,
911                            &[path_idx, alias_idx],
912                            self.line,
913                        );
914                    }
915                }
916            }
917            Node::TryOperator { operand } => {
918                self.compile_node(operand)?;
919                self.compile_try_operator_cleanup()?;
920                self.chunk.emit(Op::TryUnwrap, self.line);
921            }
922            // `try* EXPR`: evaluate EXPR; on throw, run pending finally
923            // blocks up to the innermost catch barrier and rethrow the
924            // original value. On success, leave EXPR's value on the stack.
925            //
926            // Per the issue-#26 desugaring:
927            //   { let _r = try { EXPR }
928            //     guard is_ok(_r) else { throw unwrap_err(_r) }
929            //     unwrap(_r) }
930            //
931            // The bytecode realizes this directly: install a try handler
932            // around EXPR so a throw lands in our catch path, where we
933            // pre-run pending finallys and re-emit `Throw`. Skipping the
934            // intermediate Result.Ok/Err wrapping that `TryExpr` does
935            // keeps the success path a no-op (operand value passes through
936            // as-is).
937            Node::TryStar { operand } => {
938                self.compile_try_star(operand)?;
939            }
940            Node::ImplBlock { type_name, methods } => {
941                self.compile_impl_block(type_name, methods)?;
942            }
943            Node::StructDecl { name, fields, .. } => {
944                self.compile_struct_decl(name, fields)?;
945            }
946            // Metadata-only declarations: enum names, struct/interface
947            // layouts, and type aliases are pre-scanned, so they emit no
948            // bytecode and leave the operand stack untouched. Type-alias names
949            // in expression position lower to schema expressions in the
950            // `Identifier` arm above; exported aliases use a separate compact
951            // initializer so ordinary module init chunks stay within the VM's
952            // 64 KiB jump limit.
953            // `produces_value` classifies them as non-value-producing to match;
954            // contexts that require a block to yield a value (last statement of
955            // a block, match-arm body) emit their own `Nil` placeholder.
956            // Emitting one here instead left an unpopped `Nil` on the stack in
957            // every value-discarding context (`compile_top_level_declarations`
958            // pops nothing) — a latent imbalance surfaced by the #2622 balance
959            // assertion.
960            Node::EnumDecl { name, variants, .. } => {
961                let declaration = (snode.span.start, snode.span.end);
962                if !self.predeclared_enum_declarations.contains(&declaration) {
963                    self.register_enum_decl(name, variants);
964                }
965                if self.module_level {
966                    self.compile_enum_decl(name, variants)?;
967                }
968            }
969            Node::Pipeline { .. }
970            | Node::OverrideDecl { .. }
971            | Node::TypeDecl { .. }
972            | Node::InterfaceDecl { .. } => {}
973            Node::TryCatch {
974                has_catch: _,
975                body,
976                error_var,
977                error_type,
978                catch_body,
979                finally_body,
980                ..
981            } => {
982                self.compile_try_catch(body, error_var, error_type, catch_body, finally_body)?;
983            }
984            Node::TryExpr { body } => {
985                self.compile_try_expr(body)?;
986            }
987            Node::Retry { count, body } => {
988                self.compile_retry(count, body)?;
989            }
990            Node::CostRoute { options, body } => {
991                self.compile_cost_route(options, body)?;
992            }
993            Node::Parallel {
994                mode,
995                expr,
996                variable,
997                body,
998                options,
999            } => {
1000                self.compile_parallel(mode, expr, variable, body, options)?;
1001            }
1002            Node::SpawnExpr { body } => {
1003                self.compile_spawn_expr(body)?;
1004            }
1005            Node::SelectExpr {
1006                cases,
1007                timeout,
1008                default_body,
1009            } => {
1010                self.compile_select_expr(cases, timeout, default_body)?;
1011            }
1012            Node::Spread(_) => {
1013                return Err(CompileError {
1014                    message: "spread (...) can only be used inside list literals, dict literals, or function call arguments".into(),
1015                    line: self.line,
1016                });
1017            }
1018            Node::AttributedDecl { attributes, inner } => {
1019                self.compile_attributed_decl(attributes, inner)?;
1020            }
1021            Node::OrPattern(_) => {
1022                return Err(CompileError {
1023                    message: "or-pattern (|) can only appear as a match arm pattern".into(),
1024                    line: self.line,
1025                });
1026            }
1027        }
1028        Ok(())
1029    }
1030}