ReviewOS

home-lang/home

home run: interpreter can't evaluate a literal in value position (hello world → recursion-depth 512)

#75
Closed glennmichael123 opened this 11 days ago ·
11 days ago

Summary

home run <file.home> — the Home-language tree-walking interpreter — cannot run hello world. Any program that evaluates a literal in value position (a literal passed directly to print, a literal return, or a top-level let x = <literal>) aborts with:

stack overflow: expression recursion depth exceeded 512
error: RuntimeError

examples/hello.home (fn main() { print("Hello, Home!") }) fails this way, as do examples/simple_math.home and most example programs.

Repro matrix

Built zig build debug → zig-out/bin/home-debug (also repros on home-release-safe).

ProgramResult
fn main() {}✅ Success
fn main() { let x = 5 print(x) }✅ Success — literal bound, printed via variable
fn main() { let a = "x" let b = a print(b) }✅ Success
fn main() { print("hi") }❌ recursion-depth 512
fn main() { print(42) }❌ recursion-depth 512
fn main() { let x = "hi" }❌ recursion-depth 512
fn main(): string { return "hi" }❌ recursion-depth 512

The discriminator is a literal evaluated as a value that is consumed directly (call argument, return operand, terminal let initializer). Passing the same value through a variable binding first works, and empty bodies work.

Mechanism / where to look

  • Guard/counter: packages/interpreter/src/interpreter.zig:1086-1092 (evaluateExpression, recursion_depth += 1; defer recursion_depth -= 1; if (> MAX_RECURSION_DEPTH=512) …). The counter itself is balanced (increment + defer decrement), so the 512 trip is a genuine non-terminating re-dispatch, not a leaked counter.
  • The literal arms are trivial and do not self-recurse:
    • .IntegerLiteral — interpreter.zig:1094
    • .StringLiteral — interpreter.zig:1142 (processStringEscapes then return)
  • So the infinite recursion is in the consumer path that re-feeds a literal back into evaluateExpression without progress. Candidate dispatch sites:
    • evaluateCallExpression — interpreter.zig:2550, builtin print at interpreter.zig:3049 / interpreter.zig:11132
    • the return-statement and terminal-let evaluation paths

A likely shape: an argument/return-value coercion or "evaluate then re-evaluate to coerce" step that calls evaluateExpression on the same *ast.Expr again when the value is a bare literal (but is short-circuited when the operand is already a resolved variable value).

Impact

  • Home cannot run print("…") — i.e. hello world — via home run. Every example that prints a literal is broken.
  • Blocks any interpreter-level perf work (can't benchmark a path that can't evaluate a literal). Found while doing perf recon on the interpreter.

Notes

  • The native-compile path (home build) is not covered by this report — this is specifically the home run interpreter.
  • Recent parser commits (c73c0bfcd fn forward decls, eba11d36c while-continue, 9d8b7c14e error-union sugar) touched AST shape; worth checking whether one of them altered literal-node handling and regressed this, vs. a long-standing latent bug.
11 days ago

Fixed in 72316cbca (pushed to main).

Root cause: Interpreter.init allocates with allocator.create (uninitialized memory) and sets fields one-by-one, so the recursion_depth: u32 = 0 struct-field default never applied — the field held garbage (~0xAA…) > MAX_RECURSION_DEPTH (512). The first evaluateExpression call therefore tripped the guard, which is why the failing cases all evaluate an expression while empty/parse-failed programs (which never call evaluateExpression) falsely reported success. My earlier 'literal vs variable' discriminator was an artifact: the 'passing' multi-statement-on-one-line programs were actually failing to parse (missing statement separators) and running nothing.

Fix: set recursion_depth = 0 explicitly in init. Added a program-level regression suite (lex→parse→interpret) covering hello world, literals, let-bindings, arithmetic and a returning function; verified home run examples/hello.home now prints Hello, Home!.

Note (separate, out of scope here): the runtime recursion guard at 512 is effectively unreachable for pure expression nesting — the parser caps nesting at 256 (EP0001) first — and genuine function self-recursion exhausts the native stack before 512. Worth a follow-up to reconcile those limits, but it does not affect this fix.

Sign in to comment on this issue.