@@ -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).
118118current_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`.
122128defining_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
0 commit comments