From 8e32074adeb8a3c0682a54f0912280bce482240e Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 24 Jul 2025 22:21:49 +0200 Subject: [PATCH 1/4] suggest different arity fns when it makes sense --- compiler/ml/typecore.ml | 99 ++++++++++++++++--- compiler/ml/typecore.mli | 9 +- .../suggest_existing_arity_fn_1.res.expected | 13 +++ .../suggest_existing_arity_fn_2.res.expected | 13 +++ .../suggest_existing_arity_fn_3.res.expected | 13 +++ .../fixtures/suggest_existing_arity_fn_1.res | 1 + .../fixtures/suggest_existing_arity_fn_2.res | 1 + .../fixtures/suggest_existing_arity_fn_3.res | 1 + 8 files changed, 137 insertions(+), 13 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/suggest_existing_arity_fn_1.res.expected create mode 100644 tests/build_tests/super_errors/expected/suggest_existing_arity_fn_2.res.expected create mode 100644 tests/build_tests/super_errors/expected/suggest_existing_arity_fn_3.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_1.res create mode 100644 tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_2.res create mode 100644 tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_3.res diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index feff8336c2..af1e39d8cd 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -79,8 +79,13 @@ type error = | Unknown_literal of string * char | Illegal_letrec_pat | Empty_record_literal - | Uncurried_arity_mismatch of - type_expr * int * int * Asttypes.Noloc.arg_label list + | Uncurried_arity_mismatch of { + function_type: type_expr; + expected_arity: int; + provided_arity: int; + provided_args: Asttypes.Noloc.arg_label list; + function_name: Longident.t option; + } | Field_not_optional of string * type_expr | Type_params_not_supported of Longident.t | Field_access_on_dict_type @@ -2220,6 +2225,11 @@ let not_function env ty = let ls, tvar = list_labels env ty in ls = [] && not tvar +let extract_function_name funct = + match funct.exp_desc with + | Texp_ident (path, _, _) -> Some (Longident.parse (Path.name path)) + | _ -> None + type lazy_args = (Asttypes.Noloc.arg_label * (unit -> Typedtree.expression) option) list @@ -3512,10 +3522,13 @@ and type_application ~context total_app env funct (sargs : sargs) : ( funct.exp_loc, env, Uncurried_arity_mismatch - ( funct.exp_type, - arity, - List.length sargs, - sargs |> List.map (fun (a, _) -> to_noloc a) ) )); + { + function_type = funct.exp_type; + expected_arity = arity; + provided_arity = List.length sargs; + provided_args = sargs |> List.map (fun (a, _) -> to_noloc a); + function_name = extract_function_name funct; + } )); arity | None -> max_int in @@ -3531,10 +3544,13 @@ and type_application ~context total_app env funct (sargs : sargs) : ( funct.exp_loc, env, Uncurried_arity_mismatch - ( funct.exp_type, - required_args + newarity, - required_args, - sargs |> List.map (fun (a, _) -> to_noloc a) ) ))); + { + function_type = funct.exp_type; + expected_arity = required_args + newarity; + provided_arity = required_args; + provided_args = sargs |> List.map (fun (a, _) -> to_noloc a); + function_name = extract_function_name funct; + } ))); let new_t = if fully_applied then new_t else @@ -4232,6 +4248,40 @@ let spellcheck ppf unbound_name valid_names = let spellcheck_idents ppf unbound valid_idents = spellcheck ppf (Ident.name unbound) (List.map Ident.name valid_idents) +let strip_arity_suffix name = + let len = String.length name in + let rec scan_back i = + if i < 0 || name.[i] < '0' || name.[i] > '9' then i + 1 + else scan_back (i - 1) + in + let start_of_digits = scan_back (len - 1) in + if start_of_digits > 0 && start_of_digits < len then + String.sub name 0 start_of_digits + else name + +let find_arity_suggestion env function_name target_arity = + let base_name = strip_arity_suffix function_name in + let candidate = + if target_arity = 1 then base_name + else base_name ^ string_of_int target_arity + in + try + let path, desc = Env.lookup_value (Longident.parse candidate) env in + if Builtin_attributes.deprecated_of_attrs desc.val_attributes <> None then + None + else + let expanded_type = Ctype.expand_head env desc.val_type in + let actual_arity = + match Ctype.get_arity env expanded_type with + | Some arity -> arity + | None -> 0 + in + if actual_arity = target_arity then Some (Printtyp.string_of_path path) + else None + with + | Not_found -> None + | _ -> None + open Format let longident = Printtyp.longident let super_report_unification_error = Printtyp.super_report_unification_error @@ -4491,7 +4541,14 @@ let report_error env loc ppf error = fprintf ppf "Empty record literal {} should be type annotated or used in a record \ context." - | Uncurried_arity_mismatch (typ, arity, args, sargs) -> + | Uncurried_arity_mismatch + { + function_type = typ; + expected_arity = arity; + provided_arity = args; + provided_args = sargs; + function_name = function_name_opt; + } -> (* We need: - Any arg that's required but isn't passed - Any arg that is passed but isn't in the fn definition (optional or labelled) @@ -4600,6 +4657,26 @@ let report_error env loc ppf error = (if args = 1 then "" else "s") arity; + (* Add suggestions for functions with correct arity *) + (match function_name_opt with + | Some function_name -> ( + let function_name_str = + let buffer = Buffer.create 16 in + let formatter = Format.formatter_of_buffer buffer in + Printtyp.longident formatter function_name; + Format.pp_print_flush formatter (); + Buffer.contents buffer + in + let suggestion = find_arity_suggestion env function_name_str args in + match suggestion with + | None -> () (* No suggestion found *) + | Some suggestion_str -> + fprintf ppf + "@,@,Hint: Try @{%s@} instead (takes @{%d@} argument%s)." + suggestion_str args + (if args = 1 then "" else "s")) + | None -> () (* Function name not available *)); + fprintf ppf "@]" | Field_not_optional (name, typ) -> fprintf ppf "Field @{%s@} is not optional in type %a. Use without ?" diff --git a/compiler/ml/typecore.mli b/compiler/ml/typecore.mli index f615c226ec..f018717f2d 100644 --- a/compiler/ml/typecore.mli +++ b/compiler/ml/typecore.mli @@ -111,8 +111,13 @@ type error = | Unknown_literal of string * char | Illegal_letrec_pat | Empty_record_literal - | Uncurried_arity_mismatch of - type_expr * int * int * Asttypes.Noloc.arg_label list + | Uncurried_arity_mismatch of { + function_type: type_expr; + expected_arity: int; + provided_arity: int; + provided_args: Asttypes.Noloc.arg_label list; + function_name: Longident.t option; + } | Field_not_optional of string * type_expr | Type_params_not_supported of Longident.t | Field_access_on_dict_type diff --git a/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_1.res.expected b/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_1.res.expected new file mode 100644 index 0000000000..42052aaa5d --- /dev/null +++ b/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_1.res.expected @@ -0,0 +1,13 @@ + + We've found a bug for you! + /.../fixtures/suggest_existing_arity_fn_1.res:1:1-11 + + 1 │ Console.log(1, 2) + + This function call is incorrect. + The function has type: + 'a => unit + + - The function takes just 1 unlabelled argument, but is called with 2 + + Hint: Try Console.log2 instead (takes 2 arguments). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_2.res.expected b/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_2.res.expected new file mode 100644 index 0000000000..81422b34b5 --- /dev/null +++ b/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_2.res.expected @@ -0,0 +1,13 @@ + + We've found a bug for you! + /.../fixtures/suggest_existing_arity_fn_2.res:1:1-12 + + 1 │ Console.log2(1) + + This function call is incorrect. + The function has type: + (int, 'a) => unit + + - The function takes 2 unlabelled arguments, but is called with just 1 + + Hint: Try Console.log instead (takes 1 argument). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_3.res.expected b/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_3.res.expected new file mode 100644 index 0000000000..1ba58e27d3 --- /dev/null +++ b/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_3.res.expected @@ -0,0 +1,13 @@ + + We've found a bug for you! + /.../fixtures/suggest_existing_arity_fn_3.res:1:1-12 + + 1 │ Console.log4(1, 2) + + This function call is incorrect. + The function has type: + (int, int, 'a, 'b) => unit + + - The function takes 4 unlabelled arguments, but is called with just 2 + + Hint: Try Console.log2 instead (takes 2 arguments). \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_1.res b/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_1.res new file mode 100644 index 0000000000..2a98cb0d99 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_1.res @@ -0,0 +1 @@ +Console.log(1, 2) \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_2.res b/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_2.res new file mode 100644 index 0000000000..da485f0eb3 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_2.res @@ -0,0 +1 @@ +Console.log2(1) \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_3.res b/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_3.res new file mode 100644 index 0000000000..cf41e5a933 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_3.res @@ -0,0 +1 @@ +Console.log4(1, 2) \ No newline at end of file From 1449f956fef76a9449685f56bf19b0c32225286f Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 24 Jul 2025 22:24:06 +0200 Subject: [PATCH 2/4] improve comments --- compiler/ml/typecore.ml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index af1e39d8cd..9cb385bcce 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -4657,7 +4657,7 @@ let report_error env loc ppf error = (if args = 1 then "" else "s") arity; - (* Add suggestions for functions with correct arity *) + (* Add suggestions for related functions with correct arity *) (match function_name_opt with | Some function_name -> ( let function_name_str = @@ -4669,13 +4669,13 @@ let report_error env loc ppf error = in let suggestion = find_arity_suggestion env function_name_str args in match suggestion with - | None -> () (* No suggestion found *) + | None -> () | Some suggestion_str -> fprintf ppf "@,@,Hint: Try @{%s@} instead (takes @{%d@} argument%s)." suggestion_str args (if args = 1 then "" else "s")) - | None -> () (* Function name not available *)); + | None -> ()); fprintf ppf "@]" | Field_not_optional (name, typ) -> From b3bfd850a571b5530298c8e72f8c7dfb96c9709d Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 24 Jul 2025 22:25:09 +0200 Subject: [PATCH 3/4] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 870c63b415..f5f37c27d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - Better error message if platform binaries package is not found. https://github.com/rescript-lang/rescript/pull/7698 - Hint in error for string constants matching expected variant/polyvariant constructor. https://github.com/rescript-lang/rescript/pull/7711 - Polish arity mismatch error message a bit. https://github.com/rescript-lang/rescript/pull/7709 +- Suggest related functions with the expected arity in errors when it makes sense. https://github.com/rescript-lang/rescript/pull/7712 #### :house: Internal From 427f92cb247b32a717e0feba4fffeea2515a9815 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 24 Jul 2025 22:39:29 +0200 Subject: [PATCH 4/4] format --- .../expected/suggest_existing_arity_fn_1.res.expected | 1 + .../expected/suggest_existing_arity_fn_2.res.expected | 1 + .../expected/suggest_existing_arity_fn_3.res.expected | 1 + .../super_errors/fixtures/suggest_existing_arity_fn_1.res | 2 +- .../super_errors/fixtures/suggest_existing_arity_fn_2.res | 2 +- .../super_errors/fixtures/suggest_existing_arity_fn_3.res | 2 +- 6 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_1.res.expected b/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_1.res.expected index 42052aaa5d..e6b9e2e72a 100644 --- a/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_1.res.expected +++ b/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_1.res.expected @@ -3,6 +3,7 @@ /.../fixtures/suggest_existing_arity_fn_1.res:1:1-11 1 │ Console.log(1, 2) + 2 │ This function call is incorrect. The function has type: diff --git a/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_2.res.expected b/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_2.res.expected index 81422b34b5..61d3c9523d 100644 --- a/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_2.res.expected +++ b/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_2.res.expected @@ -3,6 +3,7 @@ /.../fixtures/suggest_existing_arity_fn_2.res:1:1-12 1 │ Console.log2(1) + 2 │ This function call is incorrect. The function has type: diff --git a/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_3.res.expected b/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_3.res.expected index 1ba58e27d3..ec72b069b4 100644 --- a/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_3.res.expected +++ b/tests/build_tests/super_errors/expected/suggest_existing_arity_fn_3.res.expected @@ -3,6 +3,7 @@ /.../fixtures/suggest_existing_arity_fn_3.res:1:1-12 1 │ Console.log4(1, 2) + 2 │ This function call is incorrect. The function has type: diff --git a/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_1.res b/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_1.res index 2a98cb0d99..a1eef1ac8c 100644 --- a/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_1.res +++ b/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_1.res @@ -1 +1 @@ -Console.log(1, 2) \ No newline at end of file +Console.log(1, 2) diff --git a/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_2.res b/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_2.res index da485f0eb3..9440f74dbd 100644 --- a/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_2.res +++ b/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_2.res @@ -1 +1 @@ -Console.log2(1) \ No newline at end of file +Console.log2(1) diff --git a/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_3.res b/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_3.res index cf41e5a933..342368e53f 100644 --- a/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_3.res +++ b/tests/build_tests/super_errors/fixtures/suggest_existing_arity_fn_3.res @@ -1 +1 @@ -Console.log4(1, 2) \ No newline at end of file +Console.log4(1, 2)