Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 14 additions & 15 deletions src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6633,11 +6633,11 @@ impl Infer {
let lt = self.infer_expr(lhs, env)?;
let (lb, lu) = self
.expect_num(&lt, lhs.span())
.map_err(|e| self.string_operator_hint(op, &lt, e))?;
.map_err(|e| self.operator_hint(op, &lt, e))?;
let rt = self.infer_expr(rhs, env)?;
let (rb, ru) = self
.expect_num(&rt, rhs.span())
.map_err(|e| self.string_operator_hint(op, &rt, e))?;
.map_err(|e| self.operator_hint(op, &rt, e))?;
let base_clash = |this: &Self| {
mismatch(
&this.num_ty(lb, this.apply_unit(&lu)),
Expand Down Expand Up @@ -6666,25 +6666,24 @@ impl Infer {
}
}

/// Turn generic numeric operand errors into string operator hints.
/// `+` is numeric and does not concatenate strings; `*` does not repeat them.
/// Other operands and operators retain the original error.
fn string_operator_hint(&self, op: BinOp, operand: &Ty, mut err: TypeError) -> TypeError {
if !matches!(self.apply(operand), Ty::Str) {
return err;
}

err.message = match op {
BinOp::Add => {
/// Turn a generic numeric operand error into guidance when a Python habit
/// reaches for an operator Pyfun keeps numeric: `+` does not concatenate
/// strings or lists, and `*` does not repeat strings. Every other operand and
/// operator keeps the original error.
fn operator_hint(&self, op: BinOp, operand: &Ty, mut err: TypeError) -> TypeError {
let hint = match (op, self.apply(operand)) {
(BinOp::Add, Ty::Str) => {
"`+` is numeric and does not concatenate strings — use `String.concat a b`"
.to_string()
}
BinOp::Mul => {
(BinOp::Mul, Ty::Str) => {
"`*` is numeric and does not repeat strings — use `String.repeat count string`"
.to_string()
}
(BinOp::Add, Ty::Con(name, _)) if name == "List" => {
"`+` is numeric and does not concatenate lists — use `List.concat xs ys`"
}
_ => return err,
};
err.message = hint.to_string();
err
}

Expand Down
10 changes: 9 additions & 1 deletion tests/typecheck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4020,6 +4020,12 @@ fn plus_on_strings_hints_at_string_concat() {
assert_error_contains("let s = \"a\" + 1", "String.concat");
}

#[test]
fn plus_on_lists_hints_at_list_concat() {
assert_error_contains("let xs = [1, 2] + [3]", "use `List.concat xs ys`");
assert_error_contains("let xs = 1 + [3]", "use `List.concat xs ys`");
}

#[test]
fn multiply_on_strings_hints_at_string_repeat() {
// `*` is numeric; the hint shows count-first String.repeat syntax.
Expand All @@ -4032,7 +4038,9 @@ fn a_plain_numeric_mismatch_keeps_its_message() {
// Non-string numeric mismatches must not gain string operator hints.
let add_msgs = errors("let bad = 1 + true");
assert!(
add_msgs.iter().all(|m| !m.contains("String.concat")),
add_msgs
.iter()
.all(|m| !m.contains("String.concat") && !m.contains("List.concat")),
"unexpected concat hint: {add_msgs:?}"
);
let mul_msgs = errors("let bad = 1 * true");
Expand Down
Loading