From cf40070a20dfe9504e9138386a075e2d9e2fef96 Mon Sep 17 00:00:00 2001 From: PandaHUN777 <117030640+PandaHUN777@users.noreply.github.com> Date: Mon, 28 Sep 2026 14:55:16 +0200 Subject: [PATCH] fix(typecheck): hint at String.repeat for string multiplication --- src/types/mod.rs | 31 ++++++++++++++++++++----------- tests/typecheck.rs | 20 ++++++++++++++++---- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index 0189c12..485a0d6 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -6602,11 +6602,11 @@ impl Infer { let lt = self.infer_expr(lhs, env)?; let (lb, lu) = self .expect_num(<, lhs.span()) - .map_err(|e| self.string_concat_hint(op, <, e))?; + .map_err(|e| self.string_operator_hint(op, <, 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)), @@ -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 } diff --git a/tests/typecheck.rs b/tests/typecheck.rs index 3f9cceb..08343f1 100644 --- a/tests/typecheck.rs +++ b/tests/typecheck.rs @@ -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:?}" ); }