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