Skip to content
Open
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
31 changes: 20 additions & 11 deletions src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6602,11 +6602,11 @@ impl Infer {
let lt = self.infer_expr(lhs, env)?;
let (lb, lu) = self
.expect_num(&lt, lhs.span())
.map_err(|e| self.string_concat_hint(op, &lt, e))?;
.map_err(|e| self.string_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_concat_hint(op, &rt, e))?;
.map_err(|e| self.string_operator_hint(op, &rt, e))?;
let base_clash = |this: &Self| {
mismatch(
&this.num_ty(lb, this.apply_unit(&lu)),
Expand Down Expand Up @@ -6635,16 +6635,25 @@ impl Infer {
}
}

/// Turn the generic "expected int, found string" from an `expect_num` failure
/// into a guiding hint when the user wrote `+` between strings — `+` is numeric
/// (§7.1), and `String.concat` is the concatenation path. Only augments the
/// `Add`-on-`string` case; every other numeric mismatch keeps its message.
fn string_concat_hint(&self, op: BinOp, operand: &Ty, mut err: TypeError) -> TypeError {
if op == BinOp::Add && matches!(self.apply(operand), Ty::Str) {
err.message =
"`+` is numeric and does not concatenate strings — use `String.concat a b`"
.to_string();
/// 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 => {
"`+` is numeric and does not concatenate strings — use `String.concat a b`"
.to_string()
}
BinOp::Mul => {
"`*` is numeric and does not repeat strings — use `String.repeat count string`"
.to_string()
}
_ => return err,
};
err
}

Expand Down
20 changes: 16 additions & 4 deletions tests/typecheck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3978,13 +3978,25 @@ fn plus_on_strings_hints_at_string_concat() {
assert_error_contains("let s = \"a\" + 1", "String.concat");
}

#[test]
fn multiply_on_strings_hints_at_string_repeat() {
// `*` is numeric; the hint shows count-first String.repeat syntax.
assert_error_contains("let line = \"-\" * 20", "`String.repeat count string`");
assert_error_contains("let line = 20 * \"-\"", "`String.repeat count string`");
}

#[test]
fn a_plain_numeric_mismatch_keeps_its_message() {
// A non-string numeric mismatch must NOT gain a spurious concat hint.
let msgs = errors("let bad = 1 + true");
// 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")),
"unexpected concat hint: {add_msgs:?}"
);
let mul_msgs = errors("let bad = 1 * true");
assert!(
msgs.iter().all(|m| !m.contains("String.concat")),
"unexpected concat hint: {msgs:?}"
mul_msgs.iter().all(|m| !m.contains("String.repeat")),
"unexpected repeat hint: {mul_msgs:?}"
);
}

Expand Down