Skip to content

Commit 9099b51

Browse files
Move circular definition detection from runtime to compile-time
Based on PR feedback, circular definitions like `a = a` are now detected during type checking rather than at runtime in the interpreter. Changes: - Add `circular_def` problem type to Check with clear error message - Detect circular non-function definitions in the `.processing` case when checking local lookups - Allow recursive functions (lambdas and closures) to reference themselves - Keep runtime check as safety net for edge cases The compile-time check provides better error messages and catches the issue earlier in the compilation pipeline. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent a120549 commit 9099b51

File tree

4 files changed

+91
-7
lines changed

4 files changed

+91
-7
lines changed

src/check/Check.zig

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3359,7 +3359,36 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected)
33593359
// TODO: Handle mutually recursive functions
33603360
},
33613361
.processing => {
3362-
// Recursive reference - the pattern variable is still at
3362+
// Recursive reference - check if this is a valid recursive function
3363+
// or an invalid circular value definition like `a = a`
3364+
const def = self.cir.store.getDef(processing_def.def_idx);
3365+
const def_expr = self.cir.store.getExpr(def.expr);
3366+
3367+
// Check if this is a function definition (lambda or closure)
3368+
// Closures are lambdas that capture variables from their environment
3369+
const is_function = isLambdaExpr(def_expr) or def_expr == .e_closure;
3370+
3371+
if (!is_function) {
3372+
// This is a circular value definition (not a function)
3373+
// Report an error and mark this as an error type
3374+
const pattern = self.cir.store.getPattern(def.pattern);
3375+
const ident = switch (pattern) {
3376+
.assign => |assign| assign.ident,
3377+
else => null,
3378+
};
3379+
3380+
if (ident) |ident_idx| {
3381+
_ = try self.problems.appendProblem(self.gpa, .{ .circular_def = .{
3382+
.ident = ident_idx,
3383+
.region = expr_region,
3384+
} });
3385+
}
3386+
3387+
// Unify with error type to prevent further issues
3388+
try self.unifyWith(expr_var, .err, env);
3389+
return false;
3390+
}
3391+
// For recursive functions, continue - the pattern variable is still at
33633392
// top_level rank (not generalized), so the code below will
33643393
// unify directly with it, which is the correct behavior.
33653394
},

src/check/problem.zig

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ pub const Problem = union(enum) {
5858
non_exhaustive_match: NonExhaustiveMatch,
5959
redundant_pattern: RedundantPattern,
6060
unmatchable_pattern: UnmatchablePattern,
61+
circular_def: CircularDef,
6162
bug: Bug,
6263

6364
pub const Idx = enum(u32) { _ };
@@ -99,6 +100,14 @@ pub const ComptimeEvalError = struct {
99100
region: base.Region,
100101
};
101102

103+
/// A circular definition where a value references itself (e.g., `a = a`)
104+
pub const CircularDef = struct {
105+
/// The identifier of the variable with the circular definition
106+
ident: Ident.Idx,
107+
/// The region of the circular reference
108+
region: base.Region,
109+
};
110+
102111
/// A problem involving a single type variable, with a snapshot for error reporting.
103112
/// Used for recursion errors, invalid extension types, etc.
104113
pub const VarWithSnapshot = struct {
@@ -587,6 +596,7 @@ pub const ReportBuilder = struct {
587596
.non_exhaustive_match => |data| return self.buildNonExhaustiveMatchReport(data),
588597
.redundant_pattern => |data| return self.buildRedundantPatternReport(data),
589598
.unmatchable_pattern => |data| return self.buildUnmatchablePatternReport(data),
599+
.circular_def => |data| return self.buildCircularDefReport(data),
590600
.bug => |_| return self.buildUnimplementedReport("bug"),
591601
}
592602
}
@@ -3307,6 +3317,38 @@ pub const ReportBuilder = struct {
33073317
return report;
33083318
}
33093319

3320+
/// Build a report for circular definition (e.g., `a = a`)
3321+
fn buildCircularDefReport(self: *Self, data: CircularDef) !Report {
3322+
var report = Report.init(self.gpa, "CIRCULAR DEFINITION", .runtime_error);
3323+
errdefer report.deinit();
3324+
3325+
const owned_name = try report.addOwnedString(self.can_ir.getIdentText(data.ident));
3326+
3327+
try report.document.addText("This value references itself in its own definition:");
3328+
try report.document.addLineBreak();
3329+
3330+
// Add source region highlighting
3331+
const region_info = self.module_env.calcRegionInfo(data.region);
3332+
try report.document.addSourceRegion(
3333+
region_info,
3334+
.error_highlight,
3335+
self.filename,
3336+
self.source,
3337+
self.module_env.getLineStarts(),
3338+
);
3339+
try report.document.addLineBreak();
3340+
3341+
try report.document.addText("The definition of ");
3342+
try report.document.addAnnotated(owned_name, .inline_code);
3343+
try report.document.addText(" depends on itself, which creates an infinite loop.");
3344+
try report.document.addLineBreak();
3345+
try report.document.addLineBreak();
3346+
try report.document.addAnnotated("Hint:", .emphasized);
3347+
try report.document.addText(" Recursive functions are allowed, but values cannot directly reference themselves.");
3348+
3349+
return report;
3350+
}
3351+
33103352
fn buildNonExhaustiveMatchReport(self: *Self, data: NonExhaustiveMatch) !Report {
33113353
var report = Report.init(self.gpa, "NON-EXHAUSTIVE MATCH", .runtime_error);
33123354
errdefer report.deinit();
1.1 KB
Binary file not shown.

test/snapshots/nominal/associated_items_truly_comprehensive.md

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ UNDEFINED VARIABLE - associated_items_truly_comprehensive.md:382:20:382:24
483483
UNUSED VARIABLE - associated_items_truly_comprehensive.md:382:20:382:24
484484
UNDEFINED VARIABLE - associated_items_truly_comprehensive.md:388:12:388:16
485485
UNUSED VARIABLE - associated_items_truly_comprehensive.md:388:12:388:16
486+
CIRCULAR DEFINITION - associated_items_truly_comprehensive.md:170:16:170:38
486487
# PROBLEMS
487488
**UNDEFINED VARIABLE**
488489
Nothing is named `val4` in this scope.
@@ -530,6 +531,18 @@ The unused variable is declared here:
530531
^^^^
531532

532533

534+
**CIRCULAR DEFINITION**
535+
This value references itself in its own definition:
536+
**associated_items_truly_comprehensive.md:170:16:170:38:**
537+
```roc
538+
val2 = D3_Pattern2.L2.L3.val3 + 10 # Forward ref to L3 val (qualified)
539+
```
540+
^^^^^^^^^^^^^^^^^^^^^^
541+
542+
The definition of `associated_items_truly_comprehensive.D3_Pattern2.L2.L3.val3` depends on itself, which creates an infinite loop.
543+
544+
**Hint:** Recursive functions are allowed, but values cannot directly reference themselves.
545+
533546
# TOKENS
534547
~~~zig
535548
UpperIdent,OpColonEqual,OpenSquare,UpperIdent,CloseSquare,Dot,OpenCurly,
@@ -3611,9 +3624,9 @@ anno2 = Annotated.L2.alsoTyped # 889
36113624
(patt (type "e where [e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)]), e.plus : e, f -> e, f.from_numeral : Numeral -> Try(f, [InvalidNumeral(Str)])]"))
36123625
(patt (type "e where [e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)])]"))
36133626
(patt (type "e where [e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)]), e.plus : e, f -> e, f.from_numeral : Numeral -> Try(f, [InvalidNumeral(Str)])]"))
3614-
(patt (type "e where [e.plus : e, e -> e]"))
3615-
(patt (type "e where [e.plus : e, e -> e]"))
3616-
(patt (type "e where [e.plus : e, e -> e]"))
3627+
(patt (type "Error"))
3628+
(patt (type "Error"))
3629+
(patt (type "Error"))
36173630
(patt (type "e where [e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)]), e.times : e, f -> e, f.from_numeral : Numeral -> Try(f, [InvalidNumeral(Str)])]"))
36183631
(patt (type "e where [e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)]), e.times : e, f -> e, f.from_numeral : Numeral -> Try(f, [InvalidNumeral(Str)])]"))
36193632
(patt (type "e where [e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)]), e.times : e, f -> e, f.from_numeral : Numeral -> Try(f, [InvalidNumeral(Str)])]"))
@@ -3945,9 +3958,9 @@ anno2 = Annotated.L2.alsoTyped # 889
39453958
(expr (type "e where [e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)]), e.plus : e, f -> e, f.from_numeral : Numeral -> Try(f, [InvalidNumeral(Str)])]"))
39463959
(expr (type "e where [e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)])]"))
39473960
(expr (type "e where [e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)]), e.plus : e, f -> e, f.from_numeral : Numeral -> Try(f, [InvalidNumeral(Str)])]"))
3948-
(expr (type "e where [e.plus : e, e -> e]"))
3949-
(expr (type "e where [e.plus : e, e -> e]"))
3950-
(expr (type "e where [e.plus : e, e -> e]"))
3961+
(expr (type "Error"))
3962+
(expr (type "Error"))
3963+
(expr (type "Error"))
39513964
(expr (type "e where [e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)]), e.times : e, f -> e, f.from_numeral : Numeral -> Try(f, [InvalidNumeral(Str)])]"))
39523965
(expr (type "e where [e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)]), e.times : e, f -> e, f.from_numeral : Numeral -> Try(f, [InvalidNumeral(Str)])]"))
39533966
(expr (type "e where [e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)]), e.times : e, f -> e, f.from_numeral : Numeral -> Try(f, [InvalidNumeral(Str)])]"))

0 commit comments

Comments
 (0)