Skip to content

Commit cc820f1

Browse files
authored
Merge pull request #9063 from roc-lang/fix-issue-9043
Fix crash when using var in tuple pattern
2 parents 26af747 + 3e25660 commit cc820f1

3 files changed

Lines changed: 114 additions & 25 deletions

File tree

src/canonicalize/Can.zig

Lines changed: 100 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,15 @@ processing_alias_declarations: bool = false,
116116
/// The name of the alias currently being defined (if any).
117117
/// This allows self-references to pass through (to be caught as RECURSIVE ALIAS in Check).
118118
current_alias_name: ?Ident.Idx = null,
119-
/// The pattern being defined in the current non-lambda assignment (if any).
120-
/// Used to detect self-referential definitions like `a = a` or `a = [a]`.
121-
/// This is null when we're inside a lambda (where self-references are valid for recursion).
119+
/// The node index at which pattern definitions for the current declaration started.
120+
/// Used to detect self-referential definitions like `(_, var $n) = f($n)` where
121+
/// newly created patterns are referenced in the RHS.
122+
/// This is null when we're inside a lambda or other context where inner definitions
123+
/// are independent of outer ones.
124+
defining_patterns_start: ?u32 = null,
125+
/// The main pattern being defined (for top-level declarations where the pattern
126+
/// was created in a first pass, or for simple ident patterns).
127+
/// Used to detect self-referential definitions like `a = a`.
122128
defining_pattern: ?Pattern.Idx = null,
123129
/// The expression index of the enclosing lambda, if any.
124130
/// Used to track which lambda a return expression belongs to.
@@ -3848,6 +3854,11 @@ fn canonicalizeDeclWithAnnotation(
38483854
const trace = tracy.trace(@src());
38493855
defer trace.end();
38503856

3857+
// Save the current node count BEFORE canonicalizing the pattern.
3858+
// This allows us to detect self-references: any pattern with index >= this value
3859+
// was newly created by this declaration (as opposed to existing vars being reassigned).
3860+
const patterns_start_idx: u32 = @intCast(self.env.store.nodes.len());
3861+
38513862
// Either find the placeholder pattern insert in the first past if ident,
38523863
// otherwise canonicalize the pattern
38533864
const pattern = self.parse_ir.store.getPattern(decl.pattern);
@@ -3870,19 +3881,24 @@ fn canonicalizeDeclWithAnnotation(
38703881
};
38713882

38723883
// Check if the RHS is a lambda - if so, self-references are valid (for recursion).
3873-
// Otherwise, set defining_pattern so lookups can detect invalid self-references.
3884+
// Otherwise, set defining_patterns_start and defining_pattern for self-reference detection.
38743885
const ast_body_expr = self.parse_ir.store.getExpr(decl.body);
38753886
const is_lambda = ast_body_expr == .lambda;
38763887

3877-
// Save and set defining_pattern for self-reference detection
3888+
// Save and set self-reference tracking for issues #8831, #9043:
3889+
// - defining_pattern: the main pattern (handles `a = a` for top-level placeholders)
3890+
// - defining_patterns_start: node index for new patterns (handles tuple cases)
3891+
const saved_defining_patterns_start = self.defining_patterns_start;
38783892
const saved_defining_pattern = self.defining_pattern;
38793893
if (!is_lambda) {
3894+
self.defining_patterns_start = patterns_start_idx;
38803895
self.defining_pattern = pattern_idx;
38813896
}
38823897

38833898
const can_expr = try self.canonicalizeExprOrMalformed(decl.body);
38843899

3885-
// Restore defining_pattern
3900+
// Restore self-reference tracking
3901+
self.defining_patterns_start = saved_defining_patterns_start;
38863902
self.defining_pattern = saved_defining_pattern;
38873903

38883904
const region = self.parse_ir.tokenizedRegionToRegion(decl.region);
@@ -4527,20 +4543,34 @@ pub fn canonicalizeExpr(
45274543
// Not a module-qualified lookup, or qualifier not found, proceed with normal lookup
45284544
switch (self.scopeLookup(.ident, ident)) {
45294545
.found => |found_pattern_idx| {
4530-
// Check for self-reference outside of lambda (issue #8831).
4531-
// If we're defining a non-lambda pattern and this lookup references it,
4532-
// that's an invalid self-reference like `a = a` or `a = [a]`.
4533-
if (self.defining_pattern) |defining_pat_idx| {
4534-
if (found_pattern_idx == defining_pat_idx) {
4535-
// Self-reference detected (issue #8831) - emit error and return malformed expr.
4536-
// Non-function values cannot reference themselves as that would cause
4537-
// an infinite loop at runtime.
4538-
const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .self_referential_definition = .{
4539-
.ident = ident,
4540-
.region = region,
4541-
} });
4542-
return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = DataSpan.empty() };
4546+
// Check for self-reference outside of lambda (issues #8831, #9043).
4547+
// We detect self-reference in two cases:
4548+
// 1. The found pattern IS the main defining pattern (for simple cases like `a = a`)
4549+
// 2. The found pattern was newly created by this definition (for tuple cases
4550+
// like `(_, var $n) = f($n)` where $n is referenced before being defined)
4551+
// Note: For var reassignments like `(a, $x) = f($x)` where $x already existed,
4552+
// the existing pattern has an index < defining_patterns_start, so it's valid.
4553+
const is_self_ref = blk: {
4554+
// Check if it matches the main defining pattern (handles `a = a`)
4555+
if (self.defining_pattern) |def_pat| {
4556+
if (found_pattern_idx == def_pat) break :blk true;
4557+
}
4558+
// Check if it's a newly created pattern (handles tuple cases)
4559+
if (self.defining_patterns_start) |def_start| {
4560+
if (@intFromEnum(found_pattern_idx) >= def_start) break :blk true;
45434561
}
4562+
break :blk false;
4563+
};
4564+
4565+
if (is_self_ref) {
4566+
// Self-reference detected - emit error and return malformed expr.
4567+
// Non-function values cannot reference themselves as that would cause
4568+
// an infinite loop at runtime.
4569+
const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .self_referential_definition = .{
4570+
.ident = ident,
4571+
.region = region,
4572+
} });
4573+
return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = DataSpan.empty() };
45444574
}
45454575

45464576
// Mark this pattern as used for unused variable checking
@@ -5267,6 +5297,16 @@ pub fn canonicalizeExpr(
52675297
const body_free_vars_start = self.scratch_free_vars.top();
52685298
defer self.scratch_free_vars.clearFrom(body_free_vars_start);
52695299

5300+
// Reset self-reference tracking for the lambda body - lambda bodies
5301+
// have their own scope and shouldn't inherit self-reference detection
5302+
// from outer declarations
5303+
const saved_defining_patterns_start = self.defining_patterns_start;
5304+
const saved_defining_pattern = self.defining_pattern;
5305+
self.defining_patterns_start = null;
5306+
self.defining_pattern = null;
5307+
defer self.defining_patterns_start = saved_defining_patterns_start;
5308+
defer self.defining_pattern = saved_defining_pattern;
5309+
52705310
const can_body = try self.canonicalizeExpr(e.body) orelse {
52715311
const ast_body = self.parse_ir.store.getExpr(e.body);
52725312
const body_region = self.parse_ir.tokenizedRegionToRegion(ast_body.to_tokenized_region());
@@ -6155,6 +6195,15 @@ pub fn canonicalizeExpr(
61556195
// Save position before canonicalizing body so we can filter pattern-bound vars
61566196
const body_free_vars_start = self.scratch_free_vars.top();
61576197

6198+
// Reset self-reference tracking for the branch body - variables bound by the
6199+
// branch pattern are valid to use in the body and aren't self-references
6200+
const saved_defining_patterns_start = self.defining_patterns_start;
6201+
const saved_defining_pattern = self.defining_pattern;
6202+
self.defining_patterns_start = null;
6203+
self.defining_pattern = null;
6204+
defer self.defining_patterns_start = saved_defining_patterns_start;
6205+
defer self.defining_pattern = saved_defining_pattern;
6206+
61586207
// Canonicalize the branch's body
61596208
const can_body = try self.canonicalizeExpr(ast_branch.body) orelse {
61606209
const body = self.parse_ir.store.getExpr(ast_branch.body);
@@ -6329,6 +6378,15 @@ fn canonicalizeForLoop(
63296378
const body_free_vars_start = self.scratch_free_vars.top();
63306379
defer self.scratch_free_vars.clearFrom(body_free_vars_start);
63316380

6381+
// Reset defining_patterns_start for the loop body - variables bound by the
6382+
// loop pattern are valid to use in the body and aren't self-references
6383+
const saved_defining_patterns_start = self.defining_patterns_start;
6384+
const saved_defining_pattern = self.defining_pattern;
6385+
self.defining_patterns_start = null;
6386+
self.defining_pattern = null;
6387+
defer self.defining_patterns_start = saved_defining_patterns_start;
6388+
defer self.defining_pattern = saved_defining_pattern;
6389+
63326390
const body_expr = try self.canonicalizeExprOrMalformed(ast_body);
63336391

63346392
// Copy free vars into captures, excluding pattern-bound vars (deduplicating)
@@ -9748,6 +9806,16 @@ fn canonicalizeBlock(self: *Self, e: AST.Block) std.mem.Allocator.Error!Canonica
97489806
try self.scopeEnter(self.env.gpa, false); // false = not a function boundary
97499807
defer self.scopeExit(self.env.gpa) catch {};
97509808

9809+
// Blocks create a new scope where declarations are independent of any outer
9810+
// declarations being defined. Reset self-reference tracking to prevent false
9811+
// self-reference errors for inner declarations (issue #9043).
9812+
const saved_defining_patterns_start = self.defining_patterns_start;
9813+
const saved_defining_pattern = self.defining_pattern;
9814+
self.defining_patterns_start = null;
9815+
self.defining_pattern = null;
9816+
defer self.defining_patterns_start = saved_defining_patterns_start;
9817+
defer self.defining_pattern = saved_defining_pattern;
9818+
97519819
// Statements inside a block are in statement position.
97529820
// This is important for constructs like `if` without `else`, which are only
97539821
// valid in statement position (where their value is not used).
@@ -10837,6 +10905,11 @@ pub fn canonicalizeBlockDecl(self: *Self, d: AST.Statement.Decl, mb_last_anno: ?
1083710905
}
1083810906
}
1083910907

10908+
// Save the current node count BEFORE canonicalizing the pattern.
10909+
// This allows us to detect self-references: any pattern with index >= this value
10910+
// was newly created by this declaration (as opposed to existing vars being reassigned).
10911+
const patterns_start_idx: u32 = @intCast(self.env.store.nodes.len());
10912+
1084010913
// Regular declaration - canonicalize as usual
1084110914
const pattern_idx = try self.canonicalizePattern(d.pattern) orelse inner_blk: {
1084210915
const pattern = self.parse_ir.store.getPattern(d.pattern);
@@ -10846,20 +10919,25 @@ pub fn canonicalizeBlockDecl(self: *Self, d: AST.Statement.Decl, mb_last_anno: ?
1084610919
};
1084710920

1084810921
// Check if the RHS is a lambda - if so, self-references are valid (for recursion).
10849-
// Otherwise, set defining_pattern so lookups can detect invalid self-references.
10922+
// Otherwise, set defining_patterns_start and defining_pattern for self-reference detection.
1085010923
const ast_body_expr = self.parse_ir.store.getExpr(d.body);
1085110924
const is_lambda = ast_body_expr == .lambda;
1085210925

10853-
// Save and set defining_pattern for self-reference detection (issue #8831)
10926+
// Save and set self-reference tracking for issues #8831, #9043:
10927+
// - defining_pattern: the main pattern (handles `a = a`)
10928+
// - defining_patterns_start: node index for new patterns (handles tuple cases)
10929+
const saved_defining_patterns_start = self.defining_patterns_start;
1085410930
const saved_defining_pattern = self.defining_pattern;
1085510931
if (!is_lambda) {
10932+
self.defining_patterns_start = patterns_start_idx;
1085610933
self.defining_pattern = pattern_idx;
1085710934
}
1085810935

1085910936
// Canonicalize the decl expr
1086010937
const expr = try self.canonicalizeExprOrMalformed(d.body);
1086110938

10862-
// Restore defining_pattern
10939+
// Restore self-reference tracking
10940+
self.defining_patterns_start = saved_defining_patterns_start;
1086310941
self.defining_pattern = saved_defining_pattern;
1086410942

1086510943
// Create a declaration statement

src/check/Check.zig

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,6 @@ enclosing_func_name: ?Ident.Idx,
124124
/// Type writer for formatting types at snapshot time
125125
type_writer: types_mod.TypeWriter,
126126

127-
/// A map of rigid variables that we build up during a branch of type checking
128-
const FreeVar = struct { ident: base.Ident.Idx, var_: Var };
129-
130127
/// A def + processing data
131128
const DefProcessed = struct { def_idx: CIR.Def.Idx, status: HasProcessed };
132129

src/eval/test/eval_test.zig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2176,6 +2176,20 @@ test "issue 8831: nested self-reference in list should also error" {
21762176
, error.Crash, .no_trace);
21772177
}
21782178

2179+
test "issue 9043: self-reference in tuple pattern with var element should error" {
2180+
// Regression test for GitHub issue #9043
2181+
// A self-referential definition with a mutable variable in a tuple pattern
2182+
// like `(_, var $n) = f($n)` should produce a compile-time error.
2183+
// Previously this would crash with "e_lookup_local: definition not found".
2184+
try runExpectError(
2185+
\\{
2186+
\\ next = |idx| (idx, idx + 1)
2187+
\\ (_, var $n) = next($n)
2188+
\\ $n
2189+
\\}
2190+
, error.Crash, .no_trace);
2191+
}
2192+
21792193
test "recursive function with record - stack memory restoration (issue #8813)" {
21802194
// Test that recursive closure calls don't leak stack memory.
21812195
// If stack memory is not properly restored after closure returns,

0 commit comments

Comments
 (0)