Skip to content

Commit 9039350

Browse files
Haofeiclaude
andcommitted
feat(checker): satisfy Hashable/Eq bounds via Hash/Eq derives
Extend the generic protocol-bound check so a user struct/sum satisfies `Hashable` when it `derives(Hash)` and `Eq` when it `derives(Eq)` (or `Ord`, which implies `Eq`). Builtin hashable scalars (Int family, String, Bool, etc.) satisfy both directly, and `List`/`Option`/`Result` keys are satisfiable structurally from their element types. Give RS0032 a protocol-aware cause/fix so the suggestion is `derives(Eq, Hash)` for a Map/Set key instead of the Ord-flavored comparator wording. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent bf1ecfd commit 9039350

1 file changed

Lines changed: 77 additions & 15 deletions

File tree

crates/rsscript/src/checks/calls.rs

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,7 @@ fn check_generic_call_bounds(
14461446
if type_satisfies_protocol_bound(analyzer, function, actual, protocol) {
14471447
continue;
14481448
}
1449+
let (cause, fix) = protocol_bound_guidance(protocol, actual);
14491450
analyzer.diagnostics.push(
14501451
Diagnostic::error(
14511452
code::PROTOCOL_NOT_SATISFIED,
@@ -1455,14 +1456,8 @@ fn check_generic_call_bounds(
14551456
call_span.clone(),
14561457
"protocol not satisfied",
14571458
)
1458-
.with_cause(
1459-
"Generic protocol bounds are nominal. Use a type with a matching derive, add a compatible generic bound, or pass an explicit comparator API.",
1460-
)
1461-
.with_fix(
1462-
"satisfy_protocol_bound",
1463-
format!("Add `derives({protocol})` to `{actual}` if the compiler-owned ordering is intended, or call an API that accepts an explicit comparator."),
1464-
"manual",
1465-
),
1459+
.with_cause(cause)
1460+
.with_fix("satisfy_protocol_bound", fix, "manual"),
14661461
);
14671462
}
14681463
}
@@ -1482,6 +1477,20 @@ fn type_satisfies_protocol_bound(
14821477
if protocol == "Ord" && builtin_type_is_ord(actual_root) {
14831478
return true;
14841479
}
1480+
if (protocol == "Hashable" || protocol == "Eq") && builtin_type_is_hashable(actual_root) {
1481+
return true;
1482+
}
1483+
// `List<T>`/`Option<T>`/`Result<A, B>` are `Hashable`/`Eq` exactly when their
1484+
// element types are, so a key like `List<Coord>` is satisfiable structurally.
1485+
if (protocol == "Hashable" || protocol == "Eq")
1486+
&& matches!(actual_root, "List" | "Option" | "Result")
1487+
{
1488+
if let Some(args) = type_arg_names(strip_fresh_type(actual)) {
1489+
return args
1490+
.iter()
1491+
.all(|arg| type_satisfies_protocol_bound(analyzer, function, arg, protocol));
1492+
}
1493+
}
14851494
if function.type_params.iter().any(|param| {
14861495
param.name == actual_root
14871496
&& matches!(
@@ -1553,21 +1562,74 @@ fn check_capability_from_call(
15531562
}
15541563
}
15551564

1565+
/// Protocol-specific cause/fix text for an unsatisfied generic protocol bound.
1566+
/// `Hashable`/`Eq` are compiler-derived structural contracts (used by
1567+
/// `Map`/`Set` keys), so the suggestion points at the concrete `derives(...)`
1568+
/// list rather than the comparator wording used for `Ord`.
1569+
fn protocol_bound_guidance(protocol: &str, actual: &str) -> (&'static str, String) {
1570+
match protocol {
1571+
"Hashable" => (
1572+
"A `Map` key / `Set` element must be `Hashable` (and therefore `Eq`). Hashability is a compiler-derived structural contract: a builtin scalar key, or a managed struct/sum that derives `Eq` and `Hash`.",
1573+
format!("Add `derives(Eq, Hash)` to `{actual}` so the compiler derives a structural hash and equality, or use a hashable key type."),
1574+
),
1575+
"Eq" => (
1576+
"Equality is a compiler-derived structural contract: a builtin scalar, or a managed struct/sum that derives `Eq` (or `Ord`, which implies `Eq`).",
1577+
format!("Add `derives(Eq)` to `{actual}`, or use an equatable type."),
1578+
),
1579+
_ => (
1580+
"Generic protocol bounds are nominal. Use a type with a matching derive, add a compatible generic bound, or pass an explicit comparator API.",
1581+
format!("Add `derives({protocol})` to `{actual}` if the compiler-owned ordering is intended, or call an API that accepts an explicit comparator."),
1582+
),
1583+
}
1584+
}
1585+
15561586
fn builtin_type_is_ord(type_name: &str) -> bool {
15571587
matches!(type_name, "Int" | "String" | "Bool")
15581588
}
15591589

1590+
/// Builtin scalar types that are `Hashable`/`Eq` directly (no derive needed).
1591+
/// Mirrors the structural derive support in the analyzer (`Float` is excluded
1592+
/// because it is neither `Eq` nor `Hash`).
1593+
fn builtin_type_is_hashable(type_name: &str) -> bool {
1594+
matches!(
1595+
type_name,
1596+
"Int"
1597+
| "Int8"
1598+
| "Int16"
1599+
| "Int32"
1600+
| "Int64"
1601+
| "UInt"
1602+
| "UInt8"
1603+
| "UInt16"
1604+
| "UInt32"
1605+
| "UInt64"
1606+
| "Bool"
1607+
| "Byte"
1608+
| "Char"
1609+
| "Unit"
1610+
| "String"
1611+
)
1612+
}
1613+
1614+
/// Whether a user-declared `type_name` satisfies a compiler-derived `protocol`
1615+
/// bound. `Ord` requires `derives(Ord)`; `Hashable` requires `derives(Hash)`;
1616+
/// `Eq` requires `derives(Eq)` or `derives(Ord)` (which implies `Eq`).
15601617
fn type_derives_protocol(items: &[Item], type_name: &str, protocol: &str) -> bool {
1561-
if protocol != "Ord" {
1618+
let derive_satisfies = |derives: &[String]| -> bool {
1619+
let has = |name: &str| derives.iter().any(|derive| derive == name);
1620+
match protocol {
1621+
"Ord" => has("Ord"),
1622+
"Hashable" => has("Hash"),
1623+
"Eq" => has("Eq") || has("Ord"),
1624+
_ => false,
1625+
}
1626+
};
1627+
if !matches!(protocol, "Ord" | "Hashable" | "Eq") {
15621628
return false;
15631629
}
15641630
items.iter().any(|item| match item {
1565-
Item::Type(decl) => {
1566-
decl.name == type_name && decl.derives.iter().any(|derive| derive == "Ord")
1567-
}
1568-
Item::SumType(sum) => {
1569-
sum.name == type_name && sum.derives.iter().any(|derive| derive == "Ord")
1570-
}
1631+
Item::Type(decl) => decl.name == type_name && derive_satisfies(&decl.derives),
1632+
Item::SumType(sum) => sum.name == type_name && derive_satisfies(&sum.derives),
15711633
_ => false,
15721634
})
15731635
}

0 commit comments

Comments
 (0)