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