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