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