Error on recursive type alias

This commit is contained in:
Richard Feldman 2025-12-10 07:51:47 -05:00
parent 0b6de6c1aa
commit 945262547e
No known key found for this signature in database
4 changed files with 182 additions and 52 deletions

View file

@ -1622,32 +1622,27 @@ fn generateAnnoTypeInPlace(self: *Self, anno_idx: CIR.TypeAnno.Idx, env: *Env, c
// If so, then update this annotation to be an instance
// of this type using the same backing variable
try self.unifyWith(anno_var, blk: {
switch (this_decl.type_) {
.alias => {
// TODO: Recursion is not allowed in aliases.
//
// If this type i used anywhere,
// then the user _should_ get an
// error, but we should proactively
// emit on here too
break :blk try self.types.mkAlias(
.{ .ident_idx = this_decl.name },
this_decl.backing_var,
&.{},
);
},
.nominal => {
break :blk try self.types.mkNominal(
.{ .ident_idx = this_decl.name },
this_decl.backing_var,
&.{},
self.builtin_ctx.module_name,
false, // Default to non-opaque for error case
);
},
}
}, env);
switch (this_decl.type_) {
.alias => {
// Recursion is not allowed in aliases - emit error
_ = try self.problems.appendProblem(self.gpa, .{ .recursive_alias = .{
.type_name = this_decl.name,
.region = anno_region,
} });
try self.unifyWith(anno_var, .err, env);
return;
},
.nominal => {
// Nominal types can be recursive
try self.unifyWith(anno_var, try self.types.mkNominal(
.{ .ident_idx = this_decl.name },
this_decl.backing_var,
&.{},
self.builtin_ctx.module_name,
false, // Default to non-opaque for error case
), env);
},
}
return;
}
@ -1717,32 +1712,27 @@ fn generateAnnoTypeInPlace(self: *Self, anno_idx: CIR.TypeAnno.Idx, env: *Env, c
// If so, then update this annotation to be an instance
// of this type using the same backing variable
try self.unifyWith(anno_var, blk: {
switch (this_decl.type_) {
.alias => {
// TODO: Recursion is not allowed in aliases.
//
// If this type i used anywhere,
// then the user _should_ get an
// error, but we should proactively
// emit on here too
break :blk try self.types.mkAlias(
.{ .ident_idx = this_decl.name },
this_decl.backing_var,
anno_arg_vars,
);
},
.nominal => {
break :blk try self.types.mkNominal(
.{ .ident_idx = this_decl.name },
this_decl.backing_var,
anno_arg_vars,
self.builtin_ctx.module_name,
false, // Default to non-opaque for error case
);
},
}
}, env);
switch (this_decl.type_) {
.alias => {
// Recursion is not allowed in aliases - emit error
_ = try self.problems.appendProblem(self.gpa, .{ .recursive_alias = .{
.type_name = this_decl.name,
.region = anno_region,
} });
try self.unifyWith(anno_var, .err, env);
return;
},
.nominal => {
// Nominal types can be recursive
try self.unifyWith(anno_var, try self.types.mkNominal(
.{ .ident_idx = this_decl.name },
this_decl.backing_var,
anno_arg_vars,
self.builtin_ctx.module_name,
false, // Default to non-opaque for error case
), env);
},
}
return;
}

View file

@ -42,4 +42,5 @@ test "check tests" {
std.testing.refAllDecls(@import("test/unify_test.zig"));
std.testing.refAllDecls(@import("test/instantiate_tag_union_test.zig"));
std.testing.refAllDecls(@import("test/where_clause_test.zig"));
std.testing.refAllDecls(@import("test/recursive_alias_test.zig"));
}

View file

@ -49,6 +49,7 @@ pub const Problem = union(enum) {
negative_unsigned_int: NegativeUnsignedInt,
invalid_numeric_literal: InvalidNumericLiteral,
unused_value: UnusedValue,
recursive_alias: RecursiveAlias,
infinite_recursion: VarWithSnapshot,
anonymous_recursion: VarWithSnapshot,
invalid_number_type: VarWithSnapshot,
@ -290,6 +291,13 @@ pub const TypeApplyArityMismatch = struct {
num_actual_args: u32,
};
/// Error when a type alias references itself (aliases cannot be recursive)
/// Use nominal types (:=) for recursive types instead
pub const RecursiveAlias = struct {
type_name: base.Ident.Idx,
region: base.Region,
};
// bug //
/// A bug that occurred during unification
@ -440,6 +448,9 @@ pub const ReportBuilder = struct {
.unused_value => |data| {
return self.buildUnusedValueReport(data);
},
.recursive_alias => |data| {
return self.buildRecursiveAliasReport(data);
},
.infinite_recursion => |_| return self.buildUnimplementedReport("infinite_recursion"),
.anonymous_recursion => |_| return self.buildUnimplementedReport("anonymous_recursion"),
.invalid_number_type => |_| return self.buildUnimplementedReport("invalid_number_type"),
@ -1732,6 +1743,44 @@ pub const ReportBuilder = struct {
return report;
}
/// Build a report for when a type alias references itself recursively
fn buildRecursiveAliasReport(
self: *Self,
data: RecursiveAlias,
) !Report {
var report = Report.init(self.gpa, "RECURSIVE ALIAS", .runtime_error);
errdefer report.deinit();
// Look up display name in import mapping (handles auto-imported builtin types)
const type_name_ident = if (self.import_mapping.get(data.type_name)) |display_ident|
self.can_ir.getIdent(display_ident)
else
self.can_ir.getIdent(data.type_name);
const type_name = try report.addOwnedString(type_name_ident);
// Add source region highlighting
const region_info = self.module_env.calcRegionInfo(data.region);
try report.document.addReflowingText("The type alias ");
try report.document.addAnnotated(type_name, .type_variable);
try report.document.addReflowingText(" references itself, which is not allowed:");
try report.document.addLineBreak();
try report.document.addSourceRegion(
region_info,
.error_highlight,
self.filename,
self.source,
self.module_env.getLineStarts(),
);
try report.document.addLineBreak();
try report.document.addReflowingText("Type aliases cannot be recursive. If you need a recursive type, use a nominal type (:=) instead of an alias (:).");
try report.document.addLineBreak();
return report;
}
// static dispatch //
/// Build a report for when a type is not nominal, but you're trying to

View file

@ -0,0 +1,90 @@
//! Tests for recursive alias detection.
//!
//! Type aliases (`:`) cannot be recursive because they are transparent type synonyms.
//! Recursive types must use nominal types (`:=`) instead.
const std = @import("std");
const TestEnv = @import("./TestEnv.zig");
const testing = std.testing;
// ============================================================================
// Direct self-reference tests
// ============================================================================
test "recursive alias - direct self-reference without args" {
// Simple recursive alias: A : List(A)
const source =
\\A : List(A)
;
var test_env = try TestEnv.init("A", source);
defer test_env.deinit();
try test_env.assertFirstTypeError("RECURSIVE ALIAS");
}
test "recursive alias - direct self-reference with args (apply case)" {
// Parameterized recursive alias: Node(a) : { value: a, children: List(Node(a)) }
const source =
\\Node(a) : { value: a, children: List(Node(a)) }
;
var test_env = try TestEnv.init("Node", source);
defer test_env.deinit();
try test_env.assertFirstTypeError("RECURSIVE ALIAS");
}
// ============================================================================
// Nominal type recursion is allowed
// ============================================================================
test "nominal type - direct self-reference is allowed" {
// Nominal types can be recursive
const source =
\\Node := [Node({ value: Str, children: List(Node) })]
;
var test_env = try TestEnv.init("Node", source);
defer test_env.deinit();
// No error - nominal types can be recursive
try test_env.assertNoErrors();
}
test "nominal type with args - self-reference is allowed" {
// Parameterized nominal types can be recursive
const source =
\\Tree := [Empty, Node({ value: Str, left: Tree, right: Tree })]
;
var test_env = try TestEnv.init("Tree", source);
defer test_env.deinit();
// No error - nominal types can be recursive
try test_env.assertNoErrors();
}
// ============================================================================
// Non-recursive aliases should work
// ============================================================================
test "non-recursive alias - simple alias works" {
const source =
\\Point : { x: I64, y: I64 }
;
var test_env = try TestEnv.init("Point", source);
defer test_env.deinit();
try test_env.assertNoErrors();
}
test "non-recursive alias - parameterized alias works" {
const source =
\\Pair(a, b) : (a, b)
;
var test_env = try TestEnv.init("Pair", source);
defer test_env.deinit();
try test_env.assertNoErrors();
}
test "non-recursive alias - alias to List works" {
const source =
\\IntList : List(I64)
;
var test_env = try TestEnv.init("IntList", source);
defer test_env.deinit();
try test_env.assertNoErrors();
}