Use ordinals in list error message

This commit is contained in:
Richard Feldman 2025-06-28 22:17:26 -04:00
parent 1a7043f407
commit c0700465b4
No known key found for this signature in database
10 changed files with 225 additions and 28 deletions

View file

@ -276,9 +276,10 @@ pub fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx) void {
// anything because we already pre-unified the list's elem var with it.
const first_elem_idx = elems[0];
var last_unified_idx: CIR.Expr.Idx = first_elem_idx;
var last_unified_index: usize = 0; // Track the index for error messages
self.checkExpr(first_elem_idx);
for (elems[1..]) |elem_expr_id| {
for (elems[1..], 1..) |elem_expr_id, i| {
self.checkExpr(elem_expr_id);
// Unify each element's var with the list's elem var
@ -298,19 +299,17 @@ pub fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx) void {
// Extract snapshots from the type mismatch problem
switch (self.problems.problems.get(problem_idx)) {
.type_mismatch => |mismatch| {
// The expected type is elem_var, actual is the incompatible element
elem_var_snapshot = mismatch.expected;
incompatible_snapshot = mismatch.actual;
},
else => {
// Shouldn't happen, but handle gracefully
elem_var_snapshot = self.snapshots.createSnapshot(
self.types,
elem_var,
);
incompatible_snapshot = self.snapshots.createSnapshot(
self.types,
@enumFromInt(@intFromEnum(elem_expr_id)),
);
// For other problem types (e.g., number_does_not_fit), the original
// problem is already more specific than our generic "incompatible list
// elements" message, so we should keep it as-is and not replace it.
// Note: if an element has an error type (e.g., from a nested heterogeneous
// list), unification would succeed, not fail, so we wouldn't reach this branch.
break;
},
}
@ -328,9 +327,11 @@ pub fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx) void {
.first_elem_region = prev_region orelse list.region,
.first_elem_var = @enumFromInt(@intFromEnum(last_unified_idx)),
.first_elem_snapshot = elem_var_snapshot,
.first_elem_index = last_unified_index,
.incompatible_elem_region = incomp_region orelse list.region,
.incompatible_elem_var = @enumFromInt(@intFromEnum(elem_expr_id)),
.incompatible_elem_snapshot = incompatible_snapshot,
.incompatible_elem_index = i,
},
});
@ -341,6 +342,7 @@ pub fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx) void {
// Track the last successfully unified element
last_unified_idx = elem_expr_id;
last_unified_index = i;
}
},
.e_empty_list => |_| {},

View file

@ -221,7 +221,15 @@ pub const Problem = union(enum) {
const owned_incompatible_type = try report.addOwnedString(buf.items);
// Add description
try report.document.addText("These two elements in this list have incompatible types:");
const first_ordinal = try ordinalString(gpa, data.first_elem_index + 1);
defer gpa.free(first_ordinal);
const second_ordinal = try ordinalString(gpa, data.incompatible_elem_index + 1);
defer gpa.free(second_ordinal);
const description = try std.fmt.allocPrint(gpa, "The {s} and {s} elements in this list have incompatible types:", .{ first_ordinal, second_ordinal });
defer gpa.free(description);
const owned_description = try report.addOwnedString(description);
try report.document.addText(owned_description);
try report.document.addLineBreak();
// Show a single region spanning both mismatched elements
@ -253,7 +261,10 @@ pub const Problem = union(enum) {
try report.document.addLineBreak();
// Show the type of the first element
try report.document.addText("The first element has this type:");
const first_type_desc = try std.fmt.allocPrint(gpa, "The {s} element has this type:", .{first_ordinal});
defer gpa.free(first_type_desc);
const owned_first_type_desc = try report.addOwnedString(first_type_desc);
try report.document.addText(owned_first_type_desc);
try report.document.addLineBreak();
try report.document.addText(" ");
try report.document.addAnnotated(owned_first_type, .type_variable);
@ -261,7 +272,10 @@ pub const Problem = union(enum) {
try report.document.addLineBreak();
// Show the type of the second element
try report.document.addText("The second element has this type:");
const second_type_desc = try std.fmt.allocPrint(gpa, "However, the {s} element has this type:", .{second_ordinal});
defer gpa.free(second_type_desc);
const owned_second_type_desc = try report.addOwnedString(second_type_desc);
try report.document.addText(owned_second_type_desc);
try report.document.addLineBreak();
try report.document.addText(" ");
try report.document.addAnnotated(owned_incompatible_type, .type_variable);
@ -394,6 +408,31 @@ pub const Problem = union(enum) {
const report = Report.init(allocator, "UNIMPLEMENTED", .runtime_error);
return report;
}
fn ordinalString(allocator: Allocator, n: usize) ![]u8 {
const suffix = switch (n) {
1 => "st",
2 => "nd",
3 => "rd",
else => blk: {
// For numbers ending in 1, 2, or 3 (except 11, 12, 13), use st, nd, rd
const last_digit = n % 10;
const last_two_digits = n % 100;
if (last_two_digits >= 11 and last_two_digits <= 13) {
break :blk "th"; // 11th, 12th, 13th
} else if (last_digit == 1) {
break :blk "st"; // 21st, 31st, etc.
} else if (last_digit == 2) {
break :blk "nd"; // 22nd, 32nd, etc.
} else if (last_digit == 3) {
break :blk "rd"; // 23rd, 33rd, etc.
} else {
break :blk "th"; // 4th, 5th, ... 20th, 24th, etc.
}
},
};
return std.fmt.allocPrint(allocator, "{d}{s}", .{ n, suffix });
}
};
/// A single var problem
@ -408,9 +447,11 @@ pub const IncompatibleListElements = struct {
first_elem_region: base.Region,
first_elem_var: Var,
first_elem_snapshot: SnapshotContentIdx,
first_elem_index: usize, // 0-based index of the first element
incompatible_elem_region: base.Region,
incompatible_elem_var: Var,
incompatible_elem_snapshot: SnapshotContentIdx,
incompatible_elem_index: usize, // 0-based index of the incompatible element
};
/// A two var problem

View file

@ -9,17 +9,17 @@ type=expr
~~~
# PROBLEMS
**INCOMPATIBLE LIST ELEMENTS**
These two elements in this list have incompatible types:
The 1st and 2nd elements in this list have incompatible types:
**can_list_first_concrete.md:1:2:1:13:**
```roc
[42, "world", 3.14]
```
^^^^^^^^^^^
The first element has this type:
The 1st element has this type:
_Num(*)_
The second element has this type:
However, the 2nd element has this type:
_Str_
All elements in a list must have compatible types.

View file

@ -9,17 +9,17 @@ type=expr
~~~
# PROBLEMS
**INCOMPATIBLE LIST ELEMENTS**
These two elements in this list have incompatible types:
The 1st and 2nd elements in this list have incompatible types:
**can_list_heterogeneous.md:1:2:1:12:**
```roc
[1, "hello", 3.14]
```
^^^^^^^^^^
The first element has this type:
The 1st element has this type:
_Num(*)_
The second element has this type:
However, the 2nd element has this type:
_Str_
All elements in a list must have compatible types.

View file

@ -9,17 +9,17 @@ type=expr
~~~
# PROBLEMS
**INCOMPATIBLE LIST ELEMENTS**
These two elements in this list have incompatible types:
The 2nd and 3rd elements in this list have incompatible types:
**can_list_nested_heterogeneous.md:1:6:1:20:**
```roc
[[], [1], ["hello"]]
```
^^^^^^^^^^^^^^
The first element has this type:
The 2nd element has this type:
_List(Num(*))_
The second element has this type:
However, the 3rd element has this type:
_List(Str)_
All elements in a list must have compatible types.

View file

@ -0,0 +1,59 @@
# META
~~~ini
description=List with number literal that doesn't fit in inferred type
type=expr
~~~
# SOURCE
~~~roc
[1u8, 2u8, 300]
~~~
# PROBLEMS
**INVALID NUMBER**
This number literal is not valid: 1u8
**INVALID NUMBER**
This number literal is not valid: 2u8
**INCOMPATIBLE LIST ELEMENTS**
The 1st and 2nd elements in this list have incompatible types:
**can_list_number_doesnt_fit.md:1:2:1:10:**
```roc
[1u8, 2u8, 300]
```
^^^^^^^^
The 1st element has this type:
_Error_
However, the 2nd element has this type:
_Error_
All elements in a list must have compatible types.
# TOKENS
~~~zig
OpenSquare(1:1-1:2),Int(1:2-1:5),Comma(1:5-1:6),Int(1:7-1:10),Comma(1:10-1:11),Int(1:12-1:15),CloseSquare(1:15-1:16),EndOfFile(1:16-1:16),
~~~
# PARSE
~~~clojure
(e-list @1-1-1-16
(e-int @1-2-1-5 (raw "1u8"))
(e-int @1-7-1-10 (raw "2u8"))
(e-int @1-12-1-15 (raw "300")))
~~~
# FORMATTED
~~~roc
NO CHANGE
~~~
# CANONICALIZE
~~~clojure
(e-list @1-1-1-16 (elem-var 73) (id 77)
(elems
(e-runtime-error (tag "invalid_num_literal"))
(e-runtime-error (tag "invalid_num_literal"))
(e-int @1-12-1-15 (value "300"))))
~~~
# TYPES
~~~clojure
(expr (id 77) (type "List(Error)"))
~~~

View file

@ -9,17 +9,17 @@ type=expr
~~~
# PROBLEMS
**INCOMPATIBLE LIST ELEMENTS**
These two elements in this list have incompatible types:
The 2nd and 3rd elements in this list have incompatible types:
**can_list_triple_nested_heterogeneous.md:1:6:1:32:**
```roc
[[], [[], [1]], [[], ["hello"]]]
```
^^^^^^^^^^^^^^^^^^^^^^^^^^
The first element has this type:
The 2nd element has this type:
_List(List(Num(*)))_
The second element has this type:
However, the 3rd element has this type:
_List(List(Str))_
All elements in a list must have compatible types.

View file

@ -0,0 +1,79 @@
# META
~~~ini
description=Nested heterogeneous lists
type=expr
~~~
# SOURCE
~~~roc
[[1, "hello"], [2, 3]]
~~~
# PROBLEMS
**INCOMPATIBLE LIST ELEMENTS**
The 1st and 2nd elements in this list have incompatible types:
**can_nested_heterogeneous_lists.md:1:3:1:13:**
```roc
[[1, "hello"], [2, 3]]
```
^^^^^^^^^^
The 1st element has this type:
_Num(*)_
However, the 2nd element has this type:
_Str_
All elements in a list must have compatible types.
**INCOMPATIBLE LIST ELEMENTS**
The 1st and 2nd elements in this list have incompatible types:
**can_nested_heterogeneous_lists.md:1:2:1:22:**
```roc
[[1, "hello"], [2, 3]]
```
^^^^^^^^^^^^^^^^^^^^
The 1st element has this type:
_List(Error)_
However, the 2nd element has this type:
_List(Num(*))_
All elements in a list must have compatible types.
# TOKENS
~~~zig
OpenSquare(1:1-1:2),OpenSquare(1:2-1:3),Int(1:3-1:4),Comma(1:4-1:5),StringStart(1:6-1:7),StringPart(1:7-1:12),StringEnd(1:12-1:13),CloseSquare(1:13-1:14),Comma(1:14-1:15),OpenSquare(1:16-1:17),Int(1:17-1:18),Comma(1:18-1:19),Int(1:20-1:21),CloseSquare(1:21-1:22),CloseSquare(1:22-1:23),EndOfFile(1:23-1:23),
~~~
# PARSE
~~~clojure
(e-list @1-1-1-23
(e-list @1-2-1-14
(e-int @1-3-1-4 (raw "1"))
(e-string @1-6-1-13
(e-string-part @1-7-1-12 (raw "hello"))))
(e-list @1-16-1-22
(e-int @1-17-1-18 (raw "2"))
(e-int @1-20-1-21 (raw "3"))))
~~~
# FORMATTED
~~~roc
NO CHANGE
~~~
# CANONICALIZE
~~~clojure
(e-list @1-1-1-23 (elem-var 75) (id 79)
(elems
(e-list @1-2-1-14 (elem-var 72)
(elems
(e-int @1-3-1-4 (value "1"))
(e-string @1-6-1-13
(e-literal @1-7-1-12 (string "hello")))))
(e-list @1-16-1-22 (elem-var 76)
(elems
(e-int @1-17-1-18 (value "2"))
(e-int @1-20-1-21 (value "3"))))))
~~~
# TYPES
~~~clojure
(expr (id 79) (type "List(Error)"))
~~~

View file

@ -9,17 +9,17 @@ type=expr
~~~
# PROBLEMS
**INCOMPATIBLE LIST ELEMENTS**
These two elements in this list have incompatible types:
The 2nd and 3rd elements in this list have incompatible types:
**list_type_err.md:1:5:1:15:**
```roc
[1, 2, "hello"]
```
^^^^^^^^^^
The first element has this type:
The 2nd element has this type:
_Num(*)_
The second element has this type:
However, the 3rd element has this type:
_Str_
All elements in a list must have compatible types.

View file

@ -0,0 +1,16 @@
app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br" }
main =
# Test case 1: Simple heterogeneous list
list1 = [1, "hello", 3.14]
# Test case 2: Nested heterogeneous list - inner list has type mismatch
list2 = [[1, "hello"], [2, 3]]
# Test case 3: Deeply nested heterogeneous list
list3 = [[[1, 2], [3, 4]], [["a", "b"], ["c", "d"]]]
# Test case 4: Mixed heterogeneous issues
list4 = [[1, 2], ["hello", "world"], [3.14, 2.71]]
Stdout.line! "If this compiles, something is wrong!"