From b0211f36dd28ebc864f3a1bb84cfd5aeda1295ae Mon Sep 17 00:00:00 2001 From: Hyeseong Kim Date: Thu, 7 Nov 2024 03:25:04 +0900 Subject: [PATCH] Unified operators (#7057) Introduce unified operators, the ad-hoc specialization for primitive operators. For example adding two values, we have `+` for ints, `+.` for floats, and `++` for strings. That is because we don't allow implicit conversion or overloading for operations. It is a fundamental property of the ReScript language, but it is far from the best DX we can think of, and it became a problem when new primitives like bigint were introduced. See discussion: https://github.com/rescript-lang/rescript-compiler/issues/6525 Unified ops mitigate the problem by adding ad-hoc translation rules on applications of the core built-in operators which have a form of binary ('a -> 'a -> 'a) or unary ('a -> 'a) Translation rules should be applied in its application, in both type-level and IR(lambda)-level. The rules: 1. If the lhs type is a primitive type, unify the rhs and the result type to the lhs type. 2. If the lhs type is not a primitive type but the rhs type is, unify lhs and the result type to the rhs type. 3. If both lhs type and rhs type is not a primitive type, unify the whole types to the int. Since these are simple ad-hoc translations for primitive applications, we cannot use the result type defined in other contexts. So falling back to int type is the simplest behavior that ensures backward compatibility. You can find related definitions on `ml/unified_ops.ml` file. The actual implementation of translation is colocated into other modules. - Type-level : `ml/typecore.ml` - IR-level : `ml/translcore.ml` You can find it with the function name `translate_unified_ops` Resolved #6477 --- CHANGELOG.md | 4 + compiler/ml/translcore.ml | 110 ++++++++---- compiler/ml/typecore.ml | 99 ++++++++++- compiler/ml/unified_ops.ml | 157 ++++++++++++++++++ compiler/ml/unified_ops.mli | 20 +++ runtime/Pervasives.res | 19 ++- runtime/Pervasives_mini.res | 25 ++- runtime/rescript.json | 2 +- .../math_operator_constant.res.expected | 10 +- .../expected/math_operator_int.res.expected | 20 --- .../math_operator_string.res.expected | 16 -- .../expected/primitives1.res.expected | 18 +- .../super_errors/expected/type1.res.expected | 18 +- tests/tests/src/unified_ops_test.mjs | 73 ++++++++ tests/tests/src/unified_ops_test.res | 19 +++ 15 files changed, 495 insertions(+), 115 deletions(-) create mode 100644 compiler/ml/unified_ops.ml create mode 100644 compiler/ml/unified_ops.mli create mode 100644 tests/tests/src/unified_ops_test.mjs create mode 100644 tests/tests/src/unified_ops_test.res diff --git a/CHANGELOG.md b/CHANGELOG.md index 977198bd4c..bf761eae4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ # 12.0.0-alpha.5 (Unreleased) +#### :rocket: New Feature + +- Introduce "Unified operators" for arithmetic operators (`+`, `-`, `*`, `/`, `mod`). See https://github.com/rescript-lang/rescript-compiler/pull/7057 + # 12.0.0-alpha.4 #### :boom: Breaking Change diff --git a/compiler/ml/translcore.ml b/compiler/ml/translcore.ml index 04c80e8e09..a3f61e8990 100644 --- a/compiler/ml/translcore.ml +++ b/compiler/ml/translcore.ml @@ -49,6 +49,33 @@ let transl_extension_constructor env path ext = (* Translation of primitives *) +(** This is ad-hoc translation for unifying specific primitive operations + See [Unified_ops] module for detailed explanation. + *) +let translate_unified_ops (prim : Primitive.description) (env : Env.t) + (lhs_type : type_expr) : Lambda.primitive option = + (* lhs_type is already unified in type-level *) + let entry = Hashtbl.find_opt Unified_ops.index_by_name prim.prim_name in + match entry with + | Some {specialization} -> ( + match specialization with + | {int} + when is_base_type env lhs_type Predef.path_int + || maybe_pointer_type env lhs_type = Immediate -> + Some int + | {float = Some float} when is_base_type env lhs_type Predef.path_float -> + Some float + | {bigint = Some bigint} when is_base_type env lhs_type Predef.path_bigint + -> + Some bigint + | {string = Some string} when is_base_type env lhs_type Predef.path_string + -> + Some string + | {bool = Some bool} when is_base_type env lhs_type Predef.path_bool -> + Some bool + | {int} -> Some int) + | _ -> None + type specialized = { objcomp: Lambda.primitive; intcomp: Lambda.primitive; @@ -394,12 +421,21 @@ let specialize_comparison raise Not_found if primitive is unknown *) let specialize_primitive p env ty (* ~has_constant_constructor *) = - try - let table = Hashtbl.find comparisons_table p.prim_name in - match is_function_type env ty with - | Some (lhs, _rhs) -> specialize_comparison table env lhs - | None -> table.objcomp - with Not_found -> find_primitive p.prim_name + let fn_expr = is_function_type env ty in + let unified = + match fn_expr with + | Some (lhs, _) -> translate_unified_ops p env lhs + | None -> None + in + match unified with + | Some primitive -> primitive + | None -> ( + try + let table = Hashtbl.find comparisons_table p.prim_name in + match fn_expr with + | Some (lhs, _rhs) -> specialize_comparison table env lhs + | None -> table.objcomp + with Not_found -> find_primitive p.prim_name) (* Eta-expand a primitive *) @@ -458,32 +494,44 @@ let transl_primitive loc p env ty = let transl_primitive_application loc prim env ty args = let prim_name = prim.prim_name in - try + let unified = match args with - | [arg1; _] - when is_base_type env arg1.exp_type Predef.path_bool - && Hashtbl.mem comparisons_table prim_name -> - (Hashtbl.find comparisons_table prim_name).boolcomp - | _ -> - let has_constant_constructor = - match args with - | [_; {exp_desc = Texp_construct (_, {cstr_tag = Cstr_constant _}, _)}] - | [{exp_desc = Texp_construct (_, {cstr_tag = Cstr_constant _}, _)}; _] - | [_; {exp_desc = Texp_variant (_, None)}] - | [{exp_desc = Texp_variant (_, None)}; _] -> - true - | _ -> false - in - if has_constant_constructor then - match Hashtbl.find_opt comparisons_table prim_name with - | Some table when table.simplify_constant_constructor -> table.intcomp - | Some _ | None -> specialize_primitive prim env ty - (* ~has_constant_constructor*) - else specialize_primitive prim env ty - with Not_found -> - if String.length prim_name > 0 && prim_name.[0] = '%' then - raise (Error (loc, Unknown_builtin_primitive prim_name)); - Pccall prim + | [arg1] | [arg1; _] -> translate_unified_ops prim env arg1.exp_type + | _ -> None + in + match unified with + | Some primitive -> primitive + | None -> ( + try + match args with + | [arg1; _] + when is_base_type env arg1.exp_type Predef.path_bool + && Hashtbl.mem comparisons_table prim_name -> + (Hashtbl.find comparisons_table prim_name).boolcomp + | _ -> + let has_constant_constructor = + match args with + | [ + _; {exp_desc = Texp_construct (_, {cstr_tag = Cstr_constant _}, _)}; + ] + | [ + {exp_desc = Texp_construct (_, {cstr_tag = Cstr_constant _}, _)}; _; + ] + | [_; {exp_desc = Texp_variant (_, None)}] + | [{exp_desc = Texp_variant (_, None)}; _] -> + true + | _ -> false + in + if has_constant_constructor then + match Hashtbl.find_opt comparisons_table prim_name with + | Some table when table.simplify_constant_constructor -> table.intcomp + | Some _ | None -> specialize_primitive prim env ty + (* ~has_constant_constructor*) + else specialize_primitive prim env ty + with Not_found -> + if String.length prim_name > 0 && prim_name.[0] = '%' then + raise (Error (loc, Unknown_builtin_primitive prim_name)); + Pccall prim) (* To propagate structured constants *) diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index f4334ba9bb..404bb70f7c 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -2458,7 +2458,9 @@ and type_expect_ ?type_clash_context ?in_function ?(recarg = Rejected) env sexp in let type_clash_context = type_clash_context_from_function sexp sfunct in let args, ty_res, fully_applied = - type_application ?type_clash_context uncurried env funct sargs + match translate_unified_ops env funct sargs with + | Some (targs, result_type) -> (targs, result_type, true) + | None -> type_application ?type_clash_context uncurried env funct sargs in end_def (); unify_var env (newvar ()) funct.exp_type; @@ -3561,6 +3563,101 @@ and is_automatic_curried_application env funct = | Tarrow _ -> true | _ -> false +(** This is ad-hoc translation for unifying specific primitive operations + See [Unified_ops] module for detailed explanation. + *) +and translate_unified_ops (env : Env.t) (funct : Typedtree.expression) + (sargs : sargs) : (targs * Types.type_expr) option = + match funct.exp_desc with + | Texp_ident (path, _, _) -> ( + let entry = Hashtbl.find_opt Unified_ops.index_by_path (Path.name path) in + match (entry, sargs) with + | Some {form = Unary; specialization}, [(lhs_label, lhs_expr)] -> + let lhs = type_exp env lhs_expr in + let lhs_type = expand_head env lhs.exp_type in + let result_type = + match (lhs_type.desc, specialization) with + | Tconstr (path, _, _), _ when Path.same path Predef.path_int -> + Predef.type_int + | Tconstr (path, _, _), {bool = Some _} + when Path.same path Predef.path_bool -> + Predef.type_bool + | Tconstr (path, _, _), {float = Some _} + when Path.same path Predef.path_float -> + Predef.type_float + | Tconstr (path, _, _), {bigint = Some _} + when Path.same path Predef.path_bigint -> + Predef.type_bigint + | Tconstr (path, _, _), {string = Some _} + when Path.same path Predef.path_string -> + Predef.type_string + | _ -> + unify env lhs_type Predef.type_int; + Predef.type_int + in + let targs = [(lhs_label, Some lhs)] in + Some (targs, result_type) + | ( Some {form = Binary; specialization}, + [(lhs_label, lhs_expr); (rhs_label, rhs_expr)] ) -> + let lhs = type_exp env lhs_expr in + let lhs_type = expand_head env lhs.exp_type in + let rhs = type_exp env rhs_expr in + let rhs_type = expand_head env rhs.exp_type in + let lhs, rhs, result_type = + (* Rule 1. Try unifying to lhs *) + match (lhs_type.desc, specialization) with + | Tconstr (path, _, _), _ when Path.same path Predef.path_int -> + let rhs = type_expect env rhs_expr Predef.type_int in + (lhs, rhs, Predef.type_int) + | Tconstr (path, _, _), {bool = Some _} + when Path.same path Predef.path_bool -> + let rhs = type_expect env rhs_expr Predef.type_bool in + (lhs, rhs, Predef.type_bool) + | Tconstr (path, _, _), {float = Some _} + when Path.same path Predef.path_float -> + let rhs = type_expect env rhs_expr Predef.type_float in + (lhs, rhs, Predef.type_float) + | Tconstr (path, _, _), {bigint = Some _} + when Path.same path Predef.path_bigint -> + let rhs = type_expect env rhs_expr Predef.type_bigint in + (lhs, rhs, Predef.type_bigint) + | Tconstr (path, _, _), {string = Some _} + when Path.same path Predef.path_string -> + let rhs = type_expect env rhs_expr Predef.type_string in + (lhs, rhs, Predef.type_string) + | _ -> ( + (* Rule 2. Try unifying to rhs *) + match (rhs_type.desc, specialization) with + | Tconstr (path, _, _), _ when Path.same path Predef.path_int -> + let lhs = type_expect env lhs_expr Predef.type_int in + (lhs, rhs, Predef.type_int) + | Tconstr (path, _, _), {bool = Some _} + when Path.same path Predef.path_bool -> + let lhs = type_expect env lhs_expr Predef.type_bool in + (lhs, rhs, Predef.type_bool) + | Tconstr (path, _, _), {float = Some _} + when Path.same path Predef.path_float -> + let lhs = type_expect env lhs_expr Predef.type_float in + (lhs, rhs, Predef.type_float) + | Tconstr (path, _, _), {bigint = Some _} + when Path.same path Predef.path_bigint -> + let lhs = type_expect env lhs_expr Predef.type_bigint in + (lhs, rhs, Predef.type_bigint) + | Tconstr (path, _, _), {string = Some _} + when Path.same path Predef.path_string -> + let lhs = type_expect env lhs_expr Predef.type_string in + (lhs, rhs, Predef.type_string) + | _ -> + (* Rule 3. Fallback to int *) + let lhs = type_expect env lhs_expr Predef.type_int in + let rhs = type_expect env rhs_expr Predef.type_int in + (lhs, rhs, Predef.type_int)) + in + let targs = [(lhs_label, Some lhs); (rhs_label, Some rhs)] in + Some (targs, result_type) + | _ -> None) + | _ -> None + and type_application ?type_clash_context uncurried env funct (sargs : sargs) : targs * Types.type_expr * bool = (* funct.exp_type may be generic *) diff --git a/compiler/ml/unified_ops.ml b/compiler/ml/unified_ops.ml new file mode 100644 index 0000000000..3aaa4dd021 --- /dev/null +++ b/compiler/ml/unified_ops.ml @@ -0,0 +1,157 @@ +open Misc + +(* + Unified_ops is for specialization of some primitive operators. + + For example adding two values. We have `+` for ints, `+.` for floats, and `++` for strings. + That because we don't allow implicit conversion or overloading for operations. + + It is a fundamental property of the ReScript language, but it is far from the best DX we can think of, + and it became a problem when new primitives like bigint were introduced. + + See discussion: https://github.com/rescript-lang/rescript-compiler/issues/6525 + + Unified ops mitigate the problem by adding ad-hoc translation rules on applications of the core built-in operators + which have form of binary infix ('a -> 'a -> 'a) or unary ('a -> 'a) + + Translation rules should be applied in its application, in both type-level and IR(lambda)-level. + + The rules: + + 1. If the lhs type is a primitive type, unify the rhs and the result type to the lhs type. + 2. If the lhs type is not a primitive type but the rhs type is, unify lhs and the result type to the rhs type. + 3. If both lhs type and rhs type is not a primitive type, unify the whole types to the int. + + Since these are simple ad-hoc translations for primitive applications, we cannot use the result type defined in other contexts. + So falling back to int type is the simplest behavior that ensures backwards compatibility. + + Actual implementations of translation are colocated into core modules + + You can find it in: + - Type-level : ml/typecore.ml + - IR-level : ml/translcore.ml + + With function name "translate_unified_ops" +*) + +type form = Unary | Binary + +(* Note: unified op must support int type *) +type specialization = { + int: Lambda.primitive; + bool: Lambda.primitive option; + float: Lambda.primitive option; + bigint: Lambda.primitive option; + string: Lambda.primitive option; +} + +type entry = { + path: string; + (** TODO: Maybe it can be a Path.t in Predef instead of string *) + name: string; + form: form; + specialization: specialization; +} + +let builtin x = Primitive_modules.pervasives ^ "." ^ x + +let entries = + [| + { + path = builtin "~+"; + name = "%plus"; + form = Unary; + specialization = + { + int = Pidentity; + bool = None; + float = Some Pidentity; + bigint = Some Pidentity; + string = None; + }; + }; + { + path = builtin "~-"; + name = "%neg"; + form = Unary; + specialization = + { + int = Pnegint; + bool = None; + float = Some Pnegfloat; + bigint = Some Pnegbigint; + string = None; + }; + }; + { + path = builtin "+"; + name = "%add"; + form = Binary; + specialization = + { + int = Paddint; + bool = None; + float = Some Paddfloat; + bigint = Some Paddbigint; + string = Some Pstringadd; + }; + }; + { + path = builtin "-"; + name = "%sub"; + form = Binary; + specialization = + { + int = Psubint; + bool = None; + float = Some Psubfloat; + bigint = Some Psubbigint; + string = None; + }; + }; + { + path = builtin "*"; + name = "%mul"; + form = Binary; + specialization = + { + int = Pmulint; + bool = None; + float = Some Pmulfloat; + bigint = Some Pmulbigint; + string = None; + }; + }; + { + path = builtin "/"; + name = "%div"; + form = Binary; + specialization = + { + int = Pdivint Safe; + bool = None; + float = Some Pdivfloat; + bigint = Some Pdivbigint; + string = None; + }; + }; + { + path = builtin "mod"; + name = "%mod"; + form = Binary; + specialization = + { + int = Pmodint Safe; + bool = None; + float = Some Pmodfloat; + bigint = Some Pmodbigint; + string = None; + }; + }; + |] + +let index_by_path = + entries |> Array.map (fun entry -> (entry.path, entry)) |> create_hashtable + +let index_by_name = + entries |> Array.map (fun entry -> (entry.name, entry)) |> create_hashtable diff --git a/compiler/ml/unified_ops.mli b/compiler/ml/unified_ops.mli new file mode 100644 index 0000000000..b52e052a55 --- /dev/null +++ b/compiler/ml/unified_ops.mli @@ -0,0 +1,20 @@ +type form = Unary | Binary + +type specialization = { + int: Lambda.primitive; + bool: Lambda.primitive option; + float: Lambda.primitive option; + bigint: Lambda.primitive option; + string: Lambda.primitive option; +} + +type entry = { + path: string; + name: string; + form: form; + specialization: specialization; +} + +val index_by_path : (string, entry) Hashtbl.t + +val index_by_name : (string, entry) Hashtbl.t diff --git a/runtime/Pervasives.res b/runtime/Pervasives.res index 8f05695ddc..f3552aae89 100644 --- a/runtime/Pervasives.res +++ b/runtime/Pervasives.res @@ -40,7 +40,19 @@ external __LOC_OF__: 'a => (string, 'a) = "%loc_LOC" external __LINE_OF__: 'a => (int, 'a) = "%loc_LINE" external __POS_OF__: 'a => ((string, int, int, int), 'a) = "%loc_POS" +/* Unified operations */ + +external \"~+": 'a => 'a = "%plus" +external \"~-": 'a => 'a = "%neg" + +external \"+": ('a, 'a) => 'a = "%add" +external \"-": ('a, 'a) => 'a = "%sub" +external \"*": ('a, 'a) => 'a = "%mul" +external \"/": ('a, 'a) => 'a = "%div" +external mod: ('a, 'a) => 'a = "%mod" + /* Comparisons */ +/* Note: Later comparisons will be converted to unified operations too */ external \"=": ('a, 'a) => bool = "%equal" external \"<>": ('a, 'a) => bool = "%notequal" @@ -64,15 +76,8 @@ external \"||": (bool, bool) => bool = "%sequor" /* Integer operations */ -external \"~-": int => int = "%negint" -external \"~+": int => int = "%identity" external succ: int => int = "%succint" external pred: int => int = "%predint" -external \"+": (int, int) => int = "%addint" -external \"-": (int, int) => int = "%subint" -external \"*": (int, int) => int = "%mulint" -external \"/": (int, int) => int = "%divint" -external mod: (int, int) => int = "%modint" @deprecated("Use Core instead. This will be removed in v13") let abs = x => diff --git a/runtime/Pervasives_mini.res b/runtime/Pervasives_mini.res index 9c62e80ffe..d38b877a32 100644 --- a/runtime/Pervasives_mini.res +++ b/runtime/Pervasives_mini.res @@ -13,7 +13,25 @@ external __LOC_OF__: 'a => (string, 'a) = "%loc_LOC" external __LINE_OF__: 'a => (int, 'a) = "%loc_LINE" external __POS_OF__: 'a => ((string, int, int, int), 'a) = "%loc_POS" +/* Unified operations */ +/* + Note: + + Unified operations only work on `Pervasives`. + That means we can't rely on it when building stdlib until we remove the `Pervasives_mini`. +*/ + +external \"~+": int => int = "%identity" +external \"~-": int => int = "%negint" + +external \"+": (int, int) => int = "%addint" +external \"-": (int, int) => int = "%subint" +external \"*": (int, int) => int = "%mulint" +external \"/": (int, int) => int = "%divint" +external mod: (int, int) => int = "%modint" + /* Comparisons */ +/* Note: Later comparisons will be converted to unified operations too */ external \"=": ('a, 'a) => bool = "%equal" external \"<>": ('a, 'a) => bool = "%notequal" @@ -37,15 +55,8 @@ external \"||": (bool, bool) => bool = "%sequor" /* Integer operations */ -external \"~-": int => int = "%negint" -external \"~+": int => int = "%identity" external succ: int => int = "%succint" external pred: int => int = "%predint" -external \"+": (int, int) => int = "%addint" -external \"-": (int, int) => int = "%subint" -external \"*": (int, int) => int = "%mulint" -external \"/": (int, int) => int = "%divint" -external mod: (int, int) => int = "%modint" external land: (int, int) => int = "%andint" external lor: (int, int) => int = "%orint" diff --git a/runtime/rescript.json b/runtime/rescript.json index abb683bc73..b39c522cb5 100644 --- a/runtime/rescript.json +++ b/runtime/rescript.json @@ -19,4 +19,4 @@ "-w -3+50", "-warn-error A" ] -} \ No newline at end of file +} diff --git a/tests/build_tests/super_errors/expected/math_operator_constant.res.expected b/tests/build_tests/super_errors/expected/math_operator_constant.res.expected index f2251eee15..741b07af7a 100644 --- a/tests/build_tests/super_errors/expected/math_operator_constant.res.expected +++ b/tests/build_tests/super_errors/expected/math_operator_constant.res.expected @@ -7,14 +7,8 @@ 3 │ let x = num + 12. 4 │ - This value has type: float - But it's being used with the + operator, which works on: int - - Floats and ints have their own mathematical operators. This means you cannot add a float and an int without converting between the two. - - Possible solutions: - - Ensure all values in this calculation has the type int. You can convert between floats and ints via Belt.Float.toInt and Belt.Int.fromFloat. - - Make 12. an int by removing the dot or explicitly converting to int + This has type: float + But it's expected to have type: int You can convert float to int with Belt.Float.toInt. If this is a literal, try a number without a trailing dot (e.g. 20). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/math_operator_int.res.expected b/tests/build_tests/super_errors/expected/math_operator_int.res.expected index ebccfbecb7..e69de29bb2 100644 --- a/tests/build_tests/super_errors/expected/math_operator_int.res.expected +++ b/tests/build_tests/super_errors/expected/math_operator_int.res.expected @@ -1,20 +0,0 @@ - - We've found a bug for you! - /.../fixtures/math_operator_int.res:3:9-11 - - 1 │ let num = 0. - 2 │ - 3 │ let x = num + 12. - 4 │ - - This has type: float - But it's being used with the + operator, which works on: int - - Floats and ints have their own mathematical operators. This means you cannot add a float and an int without converting between the two. - - Possible solutions: - - Ensure all values in this calculation has the type int. You can convert between floats and ints via Belt.Float.toInt and Belt.Int.fromFloat. - - Change the operator to +., which works on float - - You can convert float to int with Belt.Float.toInt. - If this is a literal, try a number without a trailing dot (e.g. 20). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/math_operator_string.res.expected b/tests/build_tests/super_errors/expected/math_operator_string.res.expected index cb03dfac3a..e69de29bb2 100644 --- a/tests/build_tests/super_errors/expected/math_operator_string.res.expected +++ b/tests/build_tests/super_errors/expected/math_operator_string.res.expected @@ -1,16 +0,0 @@ - - We've found a bug for you! - /.../fixtures/math_operator_string.res:1:9-15 - - 1 │ let x = "hello" + "what" - 2 │ - - This has type: string - But it's being used with the + operator, which works on: int - - Are you looking to concatenate strings? Use the operator ++, which concatenates strings. - - Possible solutions: - - Change the + operator to ++ to concatenate strings instead. - - You can convert string to int with Belt.Int.fromString. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/primitives1.res.expected b/tests/build_tests/super_errors/expected/primitives1.res.expected index b1a303eccb..cc2946f160 100644 --- a/tests/build_tests/super_errors/expected/primitives1.res.expected +++ b/tests/build_tests/super_errors/expected/primitives1.res.expected @@ -1,19 +1,13 @@ We've found a bug for you! - /.../fixtures/primitives1.res:2:1-2 + /.../fixtures/primitives1.res:2:6 1 │ /* got float, wanted int */ - 2 │ 2. + 2 + 2 │ 2. + 2 3 │ - This value has type: float - But it's being used with the + operator, which works on: int - - Floats and ints have their own mathematical operators. This means you cannot add a float and an int without converting between the two. - - Possible solutions: - - Ensure all values in this calculation has the type int. You can convert between floats and ints via Belt.Float.toInt and Belt.Int.fromFloat. - - Make 2. an int by removing the dot or explicitly converting to int + This has type: int + But it's expected to have type: float - You can convert float to int with Belt.Float.toInt. - If this is a literal, try a number without a trailing dot (e.g. 20). \ No newline at end of file + You can convert int to float with Belt.Int.toFloat. + If this is a literal, try a number with a trailing dot (e.g. 20.). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/type1.res.expected b/tests/build_tests/super_errors/expected/type1.res.expected index 036daa2550..6bc3692c57 100644 --- a/tests/build_tests/super_errors/expected/type1.res.expected +++ b/tests/build_tests/super_errors/expected/type1.res.expected @@ -1,18 +1,12 @@ We've found a bug for you! - /.../fixtures/type1.res:1:9-10 + /.../fixtures/type1.res:1:14 - 1 │ let x = 2. + 2 + 1 │ let x = 2. + 2 2 │ - This value has type: float - But it's being used with the + operator, which works on: int - - Floats and ints have their own mathematical operators. This means you cannot add a float and an int without converting between the two. - - Possible solutions: - - Ensure all values in this calculation has the type int. You can convert between floats and ints via Belt.Float.toInt and Belt.Int.fromFloat. - - Make 2. an int by removing the dot or explicitly converting to int + This has type: int + But it's expected to have type: float - You can convert float to int with Belt.Float.toInt. - If this is a literal, try a number without a trailing dot (e.g. 20). \ No newline at end of file + You can convert int to float with Belt.Int.toFloat. + If this is a literal, try a number with a trailing dot (e.g. 20.). \ No newline at end of file diff --git a/tests/tests/src/unified_ops_test.mjs b/tests/tests/src/unified_ops_test.mjs new file mode 100644 index 0000000000..03f68c97fd --- /dev/null +++ b/tests/tests/src/unified_ops_test.mjs @@ -0,0 +1,73 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + + +let float = 1 + 2; + +let string = "12"; + +let bigint = 1n + 2n; + +function unknown(a, b) { + return a + b | 0; +} + +function lhsint(a, b) { + return a + b | 0; +} + +function lhsfloat(a, b) { + return a + b; +} + +function lhsbigint(a, b) { + return a + b; +} + +function lhsstring(a, b) { + return a + b; +} + +function rhsint(a, b) { + return a + b | 0; +} + +function rhsfloat(a, b) { + return a + b; +} + +function rhsbigint(a, b) { + return a + b; +} + +function rhsstring(a, b) { + return a + b; +} + +function case1(a) { + return 1 + a | 0; +} + +function case2(a, b) { + return a + "test" + b; +} + +let int = 3; + +export { + int, + float, + string, + bigint, + unknown, + lhsint, + lhsfloat, + lhsbigint, + lhsstring, + rhsint, + rhsfloat, + rhsbigint, + rhsstring, + case1, + case2, +} +/* No side effect */ diff --git a/tests/tests/src/unified_ops_test.res b/tests/tests/src/unified_ops_test.res new file mode 100644 index 0000000000..610a527a97 --- /dev/null +++ b/tests/tests/src/unified_ops_test.res @@ -0,0 +1,19 @@ +let int = 1 + 2 +let float = 1. + 2. +let string = "1" + "2" +let bigint = 1n + 2n + +let unknown = (a, b) => a + b + +let lhsint = (a: int, b) => a + b +let lhsfloat = (a: float, b) => a + b +let lhsbigint = (a: bigint, b) => a + b +let lhsstring = (a: string, b) => a + b + +let rhsint = (a, b: int) => a + b +let rhsfloat = (a, b: float) => a + b +let rhsbigint = (a, b: bigint) => a + b +let rhsstring = (a, b: string) => a + b + +let case1 = a => 1 + a +let case2 = (a, b) => a + "test" + b