mirror of
https://github.com/roc-lang/roc.git
synced 2025-12-23 08:48:03 +00:00
Error on recursive type alias
This commit is contained in:
parent
0b6de6c1aa
commit
945262547e
4 changed files with 182 additions and 52 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
90
src/check/test/recursive_alias_test.zig
Normal file
90
src/check/test/recursive_alias_test.zig
Normal 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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue