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