diff --git a/src/types/mod.rs b/src/types/mod.rs index f94c454..274a002 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -6633,11 +6633,11 @@ impl Infer { let lt = self.infer_expr(lhs, env)?; let (lb, lu) = self .expect_num(<, lhs.span()) - .map_err(|e| self.string_operator_hint(op, <, e))?; + .map_err(|e| self.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_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)), @@ -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 } diff --git a/tests/typecheck.rs b/tests/typecheck.rs index 0887fcb..c1edf20 100644 --- a/tests/typecheck.rs +++ b/tests/typecheck.rs @@ -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. @@ -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");