Skip to content

Commit 0bf8dd6

Browse files
Fix stack overflow with self-referential variable definitions
Add cycle detection to evalLookupLocal for top-level definitions to prevent infinite recursion when a variable references itself (e.g., a = a). The fix adds tracking of definitions currently being evaluated using the existing def_stack mechanism. When a lookup encounters a definition that is already in progress, it either creates a placeholder for recursive functions or triggers a crash with a clear error message for non-function circular references. Also adds a regression test for issue #8942. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4a9db50 commit 0bf8dd6

File tree

2 files changed

+50
-2
lines changed

2 files changed

+50
-2
lines changed

src/eval/interpreter.zig

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13803,11 +13803,43 @@ pub const Interpreter = struct {
1380313803
for (all_defs) |def_idx| {
1380413804
const def = self.env.store.getDef(def_idx);
1380513805
if (def.pattern == lookup.pattern_idx) {
13806+
// Check if this def is already being evaluated (to detect circular references)
13807+
for (self.def_stack.items) |entry| {
13808+
if (entry.pattern_idx == lookup.pattern_idx) {
13809+
// For recursive functions (lambdas/closures), create a placeholder
13810+
const def_expr = self.env.store.getExpr(def.expr);
13811+
if (def_expr == .e_lambda or def_expr == .e_closure) {
13812+
try self.addClosurePlaceholder(def.pattern, def.expr);
13813+
const bindings_len = self.bindings.items.len;
13814+
if (bindings_len > 0) {
13815+
const last_binding = self.bindings.items[bindings_len - 1];
13816+
if (last_binding.pattern_idx == def.pattern) {
13817+
return try self.pushCopy(last_binding.value, roc_ops);
13818+
}
13819+
}
13820+
}
13821+
// Circular reference detected for non-function definition
13822+
self.triggerCrash("Circular reference detected: this value refers to itself", false, roc_ops);
13823+
return error.Crash;
13824+
}
13825+
}
13826+
13827+
// Add to def_stack before evaluating to detect cycles
13828+
try self.def_stack.append(.{
13829+
.pattern_idx = def.pattern,
13830+
.expr_idx = def.expr,
13831+
.value = null,
13832+
});
13833+
defer _ = self.def_stack.pop();
13834+
1380613835
// For top-level recursive functions, we need to add a placeholder BEFORE
1380713836
// evaluating the lambda body, so recursive calls can find the binding.
1380813837
// This mirrors what addClosurePlaceholders does for block-level definitions.
13809-
//
13810-
// Evaluate the definition normally - no placeholder handling for now
13838+
const def_expr = self.env.store.getExpr(def.expr);
13839+
if (def_expr == .e_lambda or def_expr == .e_closure) {
13840+
try self.addClosurePlaceholder(def.pattern, def.expr);
13841+
}
13842+
1381113843
const result = try self.evalWithExpectedType(def.expr, roc_ops, null);
1381213844
try self.bindings.append(.{
1381313845
.pattern_idx = def.pattern,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# META
2+
~~~ini
3+
description=Regression test for stack overflow with self-referential variable (issue #8942)
4+
type=repl
5+
~~~
6+
# SOURCE
7+
~~~roc
8+
» a = a
9+
» a
10+
~~~
11+
# OUTPUT
12+
assigned `a`
13+
---
14+
Crash: Compile-time error encountered at runtime
15+
# PROBLEMS
16+
NIL

0 commit comments

Comments
 (0)