diff --git a/src/check/Check.zig b/src/check/Check.zig index 2ee0e8eb17..5f8a39a476 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -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; } diff --git a/src/check/mod.zig b/src/check/mod.zig index 0af37fa33b..b85d3c6804 100644 --- a/src/check/mod.zig +++ b/src/check/mod.zig @@ -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")); } diff --git a/src/check/problem.zig b/src/check/problem.zig index 767fdce16e..431198c20d 100644 --- a/src/check/problem.zig +++ b/src/check/problem.zig @@ -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 diff --git a/src/check/test/recursive_alias_test.zig b/src/check/test/recursive_alias_test.zig new file mode 100644 index 0000000000..b009d826ff --- /dev/null +++ b/src/check/test/recursive_alias_test.zig @@ -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(); +}