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 {
422 pattern,
423 value,
424 type_ann,
425 ..
426 } => {
427 let binding_type = match type_ann {
428 Some(type_ann) => Some(type_ann.clone()),
429 None => self.infer_expr_type(value),
430 };
431 self.compile_node(value)?;
432 self.emit_binding_type_assertion(pattern, type_ann.as_ref());
433 self.compile_destructuring(pattern, true, snode.span)?;
434 // A `let` is reassignable, so its initializer-inferred primitive
435 // type is only safe for typed-opcode specialization when the
436 // binding is provably monomorphic (proven by
437 // `record_monomorphic_var_bindings`, run before this scope's
438 // statements). Otherwise drop the primitive fact so arithmetic
439 // stays on the generic adaptive path, which re-checks operand
440 // shapes at runtime instead of hard-committing to `AddInt` etc.
441 let binding_type = self.gate_mutable_primitive_type(snode.span, binding_type);
442 self.record_binding_type(pattern, binding_type.clone());
443 self.maybe_register_owned_drop(pattern, binding_type.as_ref(), snode.span);
444 }
445 Node::ConstBinding {
446 pattern,
447 value,
448 type_ann,
449 ..
450 } => {
451 // `const` is an immutable binding. When its initializer is in
452 // the pure const-eval subset over a plain identifier, the
453 // typechecker has already folded it; either way the VM
454 // re-evaluates the same expression, producing the folded value
455 // byte-for-byte. Lowered immutable (destructuring allowed).
456 let binding_type = match type_ann {
457 Some(type_ann) => Some(type_ann.clone()),
458 None => self.infer_expr_type(value),
459 };
460 self.compile_node(value)?;
461 self.emit_binding_type_assertion(pattern, type_ann.as_ref());
462 self.compile_destructuring(pattern, false, snode.span)?;
463 self.record_binding_type(pattern, binding_type.clone());
464 self.maybe_register_owned_drop(pattern, binding_type.as_ref(), snode.span);
465 }
466 Node::Assignment {
467 target, value, op, ..
468 } => {
469 self.compile_assignment(target, value, op)?;
470 }
471 Node::BinaryOp { op, left, right } => {
472 self.compile_binary_op(op, left, right)?;
473 }
474 Node::UnaryOp { op, operand } => {
475 self.compile_node(operand)?;
476 match op.as_str() {
477 "-" => self.chunk.emit(Op::Negate, self.line),
478 "!" => self.chunk.emit(Op::Not, self.line),
479 _ => {}
480 }
481 }
482 Node::NonNullAssert { operand } => {
483 // `expr!` — identity when present, throws when `nil`. Leaves the
484 // (non-nil) value on the stack. `JumpIfFalse` peeks, so the
485 // `is_nil` bool is popped on both paths.
486 self.compile_node(operand)?; // [value]
487 self.chunk.emit(Op::Dup, self.line); // [value, value]
488 self.chunk.emit(Op::Nil, self.line); // [value, value, nil]
489 self.chunk.emit(Op::Equal, self.line); // [value, is_nil]
490 let present_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
491 // nil path: drop the bool, throw a structured message.
492 self.chunk.emit(Op::Pop, self.line); // [value]
493 let idx =
494 self.string_constant("non-null assertion failed: value was nil (unwrap_nil)");
495 self.chunk.emit_u16(Op::Constant, idx, self.line);
496 self.chunk.emit(Op::Throw, self.line);
497 // present path: drop the bool, leaving the value.
498 self.chunk.patch_jump(present_jump);
499 self.chunk.emit(Op::Pop, self.line); // [value]
500 }
501 Node::Ternary {
502 condition,
503 true_expr,
504 false_expr,
505 } => {
506 self.compile_node(condition)?;
507 let else_jump = self.chunk.emit_jump(Op::JumpIfFalse, self.line);
508 self.chunk.emit(Op::Pop, self.line);
509 self.compile_node(true_expr)?;
510 let end_jump = self.chunk.emit_jump(Op::Jump, self.line);
511 self.chunk.patch_jump(else_jump);
512 self.chunk.emit(Op::Pop, self.line);
513 self.compile_node(false_expr)?;
514 self.chunk.patch_jump(end_jump);
515 }
516 Node::FunctionCall { name, args, .. } => {
517 self.compile_function_call(name, args)?;
518 }
519 Node::ValueCall { callee, args } => {
520 self.compile_call_expression(callee, args)?;
521 }
522 Node::MethodCall {
523 object,
524 method,
525 args,
526 } => {
527 self.compile_method_call(object, method, args)?;
528 }
529 Node::OptionalMethodCall {
530 object,
531 method,
532 args,
533 } => {
534 self.compile_node(object)?;
535 for arg in args {
536 self.compile_node(arg)?;
537 }
538 let name_idx = self.string_constant(method);
539 self.chunk
540 .emit_method_call_opt(name_idx, args.len() as u8, self.line);
541 }
542 Node::PropertyAccess { object, property } => {
543 self.compile_property_access(object, property)?;
544 }
545 Node::OptionalPropertyAccess { object, property } => {
546 self.compile_node(object)?;
547 let idx = self.string_constant(property);
548 self.chunk.emit_u16(Op::GetPropertyOpt, idx, self.line);
549 }
550 Node::SubscriptAccess { object, index } => {
551 self.compile_node(object)?;
552 self.compile_node(index)?;
553 self.chunk.emit(Op::Subscript, self.line);
554 }
555 Node::OptionalSubscriptAccess { object, index } => {
556 self.compile_node(object)?;
557 self.compile_node(index)?;
558 self.chunk.emit(Op::SubscriptOpt, self.line);
559 }
560 Node::SliceAccess { object, start, end } => {
561 self.compile_node(object)?;
562 if let Some(s) = start {
563 self.compile_node(s)?;
564 } else {
565 self.chunk.emit(Op::Nil, self.line);
566 }
567 if let Some(e) = end {
568 self.compile_node(e)?;
569 } else {
570 self.chunk.emit(Op::Nil, self.line);
571 }
572 self.chunk.emit(Op::Slice, self.line);
573 }
574 Node::IfElse {
575 condition,
576 then_body,
577 else_body,
578 ..
579 } => {
580 self.compile_if_else(condition, then_body, else_body)?;
581 }
582 Node::WhileLoop { condition, body } => {
583 self.compile_while_loop(condition, body)?;
584 }
585 Node::ForIn {
586 pattern,
587 iterable,
588 body,
589 } => {
590 self.compile_for_in(pattern, iterable, body, snode.span)?;
591 }
592 Node::ReturnStmt { value } => {
593 self.compile_return_stmt(value)?;
594 }
595 Node::BreakStmt => {
596 self.compile_break_stmt()?;
597 }
598 Node::ContinueStmt => {
599 self.compile_continue_stmt()?;
600 }
601 Node::ListLiteral(elements) => {
602 self.compile_list_literal(elements)?;
603 }
604 Node::DictLiteral(entries) => {
605 self.compile_dict_literal(entries)?;
606 }
607 Node::InterpolatedString(segments) => {
608 self.compile_interpolated_string(segments)?;
609 }
610 Node::FnDecl {
611 name,
612 type_params,
613 params,
614 body,
615 is_stream,
616 ..
617 } => {
618 self.compile_fn_decl(name, type_params, params, body, *is_stream)?;
619 }
620 Node::ToolDecl {
621 name,
622 description,
623 params,
624 return_type,
625 body,
626 ..
627 } => {
628 self.compile_tool_decl(name, description, params, return_type, body)?;
629 }
630 Node::SkillDecl { name, fields, .. } => {
631 self.compile_skill_decl(name, fields)?;
632 }
633 Node::EvalPackDecl {
634 binding_name,
635 pack_id,
636 fields,
637 body,
638 summarize,
639 ..
640 } => {
641 self.compile_eval_pack_decl(binding_name, pack_id, fields, body, summarize, true)?;
642 }
643 Node::Closure { params, body, .. } => {
644 self.compile_closure(params, body)?;
645 }
646 Node::ThrowStmt { value } => {
647 self.compile_throw_stmt(value)?;
648 }
649 Node::MatchExpr { value, arms } => {
650 self.compile_match_expr(value, arms)?;
651 }
652 Node::RangeExpr {
653 start,
654 end,
655 inclusive,
656 } => {
657 let name_idx = self.string_constant("__range__");
658 self.chunk.emit_u16(Op::Constant, name_idx, self.line);
659 self.compile_node(start)?;
660 self.compile_node(end)?;
661 if *inclusive {
662 self.chunk.emit(Op::True, self.line);
663 } else {
664 self.chunk.emit(Op::False, self.line);
665 }
666 self.chunk.emit_u8(Op::Call, 3, self.line);
667 }
668 Node::GuardStmt {
669 condition,
670 else_body,
671 } => {
672 self.compile_guard_stmt(condition, else_body)?;
673 }
674 Node::RequireStmt { condition, message } => {
675 self.compile_node(condition)?;
676 let ok_jump = self.chunk.emit_jump(Op::JumpIfTrue, self.line);
677 self.chunk.emit(Op::Pop, self.line);
678 if let Some(message) = message {
679 self.compile_node(message)?;
680 } else {
681 let idx = self.string_constant("require condition failed");
682 self.chunk.emit_u16(Op::Constant, idx, self.line);
683 }
684 self.chunk.emit(Op::Throw, self.line);
685 self.chunk.patch_jump(ok_jump);
686 self.chunk.emit(Op::Pop, self.line);
687 }
688 Node::Block(stmts) => {
689 self.compile_scoped_block(stmts)?;
690 }
691 Node::DeadlineBlock { duration, body } => {
692 self.compile_node(duration)?;
693 self.chunk.emit(Op::DeadlineSetup, self.line);
694 self.compile_scoped_block(body)?;
695 self.chunk.emit(Op::DeadlineEnd, self.line);
696 }
697 Node::MutexBlock { key, body } => {
698 self.begin_scope();
699 let finally_floor = self.finally_bodies.len();
700 match key {
701 // `mutex(resource) { ... }`: evaluate the resource and key
702 // the lock on its structural value at runtime.
703 Some(key_expr) => {
704 self.compile_node(key_expr)?;
705 self.chunk.emit(Op::SyncMutexEnterKeyed, self.line);
706 }
707 // `mutex { ... }`: key on the lexical call-site (computed in
708 // the VM from the chunk + instruction pointer) so distinct
709 // blocks don't contend on one global lock.
710 None => {
711 self.chunk.emit(Op::SyncMutexEnter, self.line);
712 }
713 }
714 for sn in body {
715 self.compile_discarded_stmt(sn)?;
716 }
717 self.drain_finallys_to_floor(finally_floor)?;
718 self.chunk.emit(Op::Nil, self.line);
719 self.end_scope();
720 }
721 Node::ScopeBlock { body } => {
722 // Structured-concurrency nursery. `TaskScopeEnter` pushes a task
723 // scope; tasks spawned inside register to it. `TaskScopeExit`
724 // joins them (propagating the first error, cancelling the rest).
725 // On `throw`/early exit the scope is unwound and its tasks
726 // cancelled by the frame/handler teardown, mirroring
727 // `held_sync_guards`.
728 self.begin_scope();
729 let finally_floor = self.finally_bodies.len();
730 self.chunk.emit(Op::TaskScopeEnter, self.line);
731 for sn in body {
732 self.compile_discarded_stmt(sn)?;
733 }
734 self.drain_finallys_to_floor(finally_floor)?;
735 self.chunk.emit(Op::TaskScopeExit, self.line);
736 self.chunk.emit(Op::Nil, self.line);
737 self.end_scope();
738 }
739 Node::DeferStmt { body } => {
740 // Register the body to run on return/throw/scope-exit. The
741 // statement emits no bytecode of its own — the deferred body
742 // is inlined later by the finally-draining machinery — so it
743 // leaves the operand stack untouched, matching
744 // `produces_value` == false. Emitting a `Nil` here instead
745 // leaked an unpopped slot per execution, which in a loop body
746 // grew the operand stack without bound (surfaced by the
747 // #2622 balance assertion).
748 self.finally_bodies
749 .push(FinallyEntry::Finally(body.clone()));
750 }
751 Node::YieldExpr { value } => {
752 if let Some(val) = value {
753 self.compile_node(val)?;
754 } else {
755 self.chunk.emit(Op::Nil, self.line);
756 }
757 self.chunk.emit(Op::Yield, self.line);
758 }
759 Node::EmitExpr { value } => {
760 self.compile_node(value)?;
761 self.chunk.emit(Op::Yield, self.line);
762 }
763 Node::EnumConstruct {
764 enum_name,
765 variant,
766 args,
767 } => {
768 self.compile_enum_construct(enum_name, variant, args)?;
769 }
770 Node::StructConstruct {
771 struct_name,
772 fields,
773 } => {
774 self.compile_struct_construct(struct_name, fields)?;
775 }
776 Node::ImportDecl { path, .. } => {
777 let idx = self.string_constant(path);
778 self.chunk.emit_u16(Op::Import, idx, self.line);
779 }
780 Node::SelectiveImport { names, path, .. } => {
781 let path_idx = self.string_constant(path);
782 let names_str = names.join(",");
783 let names_idx = self.owned_string_constant(names_str);
784 self.chunk.emit_u16_operands(
785 Op::SelectiveImport,
786 &[path_idx, names_idx],
787 self.line,
788 );
789 }
790 Node::NamespaceImport { alias, path, .. } => {
791 let path_idx = self.string_constant(path);
792 let alias_idx = self.string_constant(alias);
793 match self.namespace_import_demands.get(alias) {
794 Some(harn_parser::NamespaceDemand::Members(members)) => {
795 let names_idx = self.owned_string_constant(
796 members.iter().cloned().collect::<Vec<_>>().join(","),
797 );
798 self.chunk.emit_u16_operands(
799 Op::NamespaceImportMembers,
800 &[path_idx, alias_idx, names_idx],
801 self.line,
802 );
803 }
804 Some(harn_parser::NamespaceDemand::Whole) | None => {
805 self.chunk.emit_u16_operands(
806 Op::NamespaceImport,
807 &[path_idx, alias_idx],
808 self.line,
809 );
810 }
811 }
812 }
813 Node::TryOperator { operand } => {
814 self.compile_node(operand)?;
815 self.chunk.emit(Op::TryUnwrap, self.line);
816 }
817 // `try* EXPR`: evaluate EXPR; on throw, run pending finally
818 // blocks up to the innermost catch barrier and rethrow the
819 // original value. On success, leave EXPR's value on the stack.
820 //
821 // Per the issue-#26 desugaring:
822 // { let _r = try { EXPR }
823 // guard is_ok(_r) else { throw unwrap_err(_r) }
824 // unwrap(_r) }
825 //
826 // The bytecode realizes this directly: install a try handler
827 // around EXPR so a throw lands in our catch path, where we
828 // pre-run pending finallys and re-emit `Throw`. Skipping the
829 // intermediate Result.Ok/Err wrapping that `TryExpr` does
830 // keeps the success path a no-op (operand value passes through
831 // as-is).
832 Node::TryStar { operand } => {
833 self.compile_try_star(operand)?;
834 }
835 Node::ImplBlock { type_name, methods } => {
836 self.compile_impl_block(type_name, methods)?;
837 }
838 Node::StructDecl { name, fields, .. } => {
839 self.compile_struct_decl(name, fields)?;
840 }
841 // Metadata-only declarations: enum names, struct/interface
842 // layouts, and type aliases are pre-scanned, so they emit no
843 // bytecode and leave the operand stack untouched. Type-alias names
844 // in expression position lower to schema expressions in the
845 // `Identifier` arm above; exported aliases use a separate compact
846 // initializer so ordinary module init chunks stay within the VM's
847 // 64 KiB jump limit.
848 // `produces_value` classifies them as non-value-producing to match;
849 // contexts that require a block to yield a value (last statement of
850 // a block, match-arm body) emit their own `Nil` placeholder.
851 // Emitting one here instead left an unpopped `Nil` on the stack in
852 // every value-discarding context (`compile_top_level_declarations`
853 // pops nothing) — a latent imbalance surfaced by the #2622 balance
854 // assertion.
855 Node::EnumDecl { name, variants, .. } => {
856 let declaration = (snode.span.start, snode.span.end);
857 if !self.predeclared_enum_declarations.contains(&declaration) {
858 self.register_enum_decl(name, variants);
859 }
860 if self.module_level {
861 self.compile_enum_decl(name, variants)?;
862 }
863 }
864 Node::Pipeline { .. }
865 | Node::OverrideDecl { .. }
866 | Node::TypeDecl { .. }
867 | Node::InterfaceDecl { .. } => {}
868 Node::TryCatch {
869 has_catch: _,
870 body,
871 error_var,
872 error_type,
873 catch_body,
874 finally_body,
875 ..
876 } => {
877 self.compile_try_catch(body, error_var, error_type, catch_body, finally_body)?;
878 }
879 Node::TryExpr { body } => {
880 self.compile_try_expr(body)?;
881 }
882 Node::Retry { count, body } => {
883 self.compile_retry(count, body)?;
884 }
885 Node::CostRoute { options, body } => {
886 self.compile_cost_route(options, body)?;
887 }
888 Node::Parallel {
889 mode,
890 expr,
891 variable,
892 body,
893 options,
894 } => {
895 self.compile_parallel(mode, expr, variable, body, options)?;
896 }
897 Node::SpawnExpr { body } => {
898 self.compile_spawn_expr(body)?;
899 }
900 Node::HitlExpr { kind, args } => {
901 self.compile_hitl_expr(*kind, args)?;
902 }
903 Node::SelectExpr {
904 cases,
905 timeout,
906 default_body,
907 } => {
908 self.compile_select_expr(cases, timeout, default_body)?;
909 }
910 Node::Spread(_) => {
911 return Err(CompileError {
912 message: "spread (...) can only be used inside list literals, dict literals, or function call arguments".into(),
913 line: self.line,
914 });
915 }
916 Node::AttributedDecl { attributes, inner } => {
917 self.compile_attributed_decl(attributes, inner)?;
918 }
919 Node::OrPattern(_) => {
920 return Err(CompileError {
921 message: "or-pattern (|) can only appear as a match arm pattern".into(),
922 line: self.line,
923 });
924 }
925 }
926 Ok(())
927 }
928}