diff --git a/analysis/src/type_utils.ml b/analysis/src/type_utils.ml index 681c8eab3e..835d449ef5 100644 --- a/analysis/src/type_utils.ml +++ b/analysis/src/type_utils.ml @@ -1330,7 +1330,9 @@ let rec find_root_type_id ~full ~env ~state (t : Types.type_expr) = then Some (if debug then Printf.printf "[find_root_type_id] returning builtin\n"; - Path.name path) + (* [char] is an alias of [int] *) + if Path.same path Predef.path_char then Path.name Predef.path_int + else Path.name path) else None) | _ -> None @@ -1396,7 +1398,7 @@ let completion_path_from_maybe_builtin path = | Some ("list", _) -> Some ["Stdlib"; "List"] | Some ("result", _) -> Some ["Stdlib"; "Result"] | Some ("dict", _) -> Some ["Stdlib"; "Dict"] - | Some ("char", _) -> Some ["Stdlib"; "Char"] + | Some ("char", _) -> Some ["Stdlib"; "Int"] | _ -> ( match path |> Utils.expand_path |> List.rev with | [main_module; "t"] when String.starts_with ~prefix:"Stdlib_" main_module diff --git a/changes/char-int-alias.feature.md b/changes/char-int-alias.feature.md new file mode 100644 index 0000000000..0287859806 --- /dev/null +++ b/changes/char-int-alias.feature.md @@ -0,0 +1 @@ +- `char` is now an alias of `int`, so char literals and ints mix freely, including in `switch` patterns. diff --git a/compiler/ml/matching.ml b/compiler/ml/matching.ml index e96eecb02a..0cdbb1b9fc 100644 --- a/compiler/ml/matching.ml +++ b/compiler/ml/matching.ml @@ -1147,28 +1147,30 @@ let add make_matching_fun division eq_key key patl_action args = cell.pm.cases <- [patl_action]; (key, cell) :: division -(* Cells whose key is [hashable] are also found by hashing, which must agree - with [eq_key]. This keeps a column of many distinct keys linear. *) -let divide ?(hashable = fun _ -> false) make eq_key get_key get_args ctx pm = +(* Cells whose key has a [hash_key] are also found by hashing on it, which + must agree with [eq_key]. This keeps a column of many distinct keys + linear. *) +let divide ?(hash_key = fun _ -> None) make eq_key get_key get_args ctx pm = let cells = lazy (Hashtbl.create 16) in let rec divide_rec = function | (p :: patl, action) :: rem -> ( let this_match = divide_rec rem in let key = get_key p in let patl_action = (get_args p patl, action) in - if not (hashable key) then + match hash_key key with + | None -> add (make p pm.default ctx) this_match eq_key key patl_action pm.args - else + | Some hkey -> ( let cells = Lazy.force cells in - match Hashtbl.find_opt cells key with + match Hashtbl.find_opt cells hkey with | Some cell -> cell.pm.cases <- patl_action :: cell.pm.cases; this_match | None -> let cell = make p pm.default ctx pm.args in cell.pm.cases <- [patl_action]; - Hashtbl.add cells key cell; - (key, cell) :: this_match) + Hashtbl.add cells hkey cell; + (key, cell) :: this_match)) | _ -> [] in @@ -1226,9 +1228,10 @@ let make_constant_matching p def ctx = function let divide_constant ctx m = divide - ~hashable:(function - | Asttypes.Const_int _ | Const_char _ | Const_string _ -> true - | Const_float _ | Const_bigint _ -> false) + ~hash_key:(function + | (Asttypes.Const_int _ | Const_char _ | Const_string _) as c -> + Some (Parmatch.const_key c) + | Const_float _ | Const_bigint _ -> None) make_constant_matching (fun c d -> const_compare c d = 0) (get_key_constant "divide") @@ -1999,11 +2002,12 @@ let combine_constant loc arg cst partial ctx def let fail, local_jumps = mk_failaction_neg partial ctx def in let lambda1 = match (cst : Asttypes.constant) with - | Const_int _ -> + | Const_int _ | Const_char _ -> + (* [char] is an alias of [int], so a column can mix both. *) let int_lambda_list = List.map (function - | Asttypes.Const_int n, l -> (n, l) + | (Asttypes.Const_int n | Const_char n), l -> (n, l) | _ -> assert false) const_lambda_list in @@ -2011,15 +2015,6 @@ let combine_constant loc arg cst partial ctx def Otherwise the upper fail interval starts at 2147483648, which wraps when converted to a Lambda integer constant. *) call_switcher fail arg int32_low int32_high int_lambda_list - | Const_char _ -> - let int_lambda_list = - List.map - (function - | Asttypes.Const_char c, l -> (c, l) - | _ -> assert false) - const_lambda_list - in - call_switcher fail arg 0 max_int int_lambda_list | Const_string _ -> (* Note as the bytecode compiler may resort to dichotomic search, the clauses of stringswitch are sorted with duplicates removed. diff --git a/compiler/ml/parmatch.ml b/compiler/ml/parmatch.ml index 3e731ed345..643be60632 100644 --- a/compiler/ml/parmatch.ml +++ b/compiler/ml/parmatch.ml @@ -155,8 +155,7 @@ let all_coherent column = | _ -> false) | Tpat_constant c1, Tpat_constant c2 -> ( match (c1, c2) with - | Const_char _, Const_char _ - | Const_int _, Const_int _ + | (Const_char _ | Const_int _), (Const_char _ | Const_int _) | Const_bigint _, Const_bigint _ | Const_float _, Const_float _ | Const_string _, Const_string _ -> @@ -270,8 +269,16 @@ let const_compare x y = | Const_bigint (s1, b1), Const_bigint (s2, b2) -> Bigint_utils.compare (s1, b1) (s2, b2) | Const_string s1, Const_string s2 -> String.compare s1 s2 + | (Const_char i1 | Const_int i1), (Const_char i2 | Const_int i2) -> + compare i1 i2 | _, _ -> compare x y +(* [char] is an alias of [int], so a char constant and an int constant with the + same value match the same values. Hash tables of constants use this key. *) +let const_key = function + | Const_char i -> Const_int i + | c -> c + let records_args l1 l2 = (* Invariant: fields are already sorted by Typecore.type_record_elem_list *) let rec combine r1 r2 l1 l2 = @@ -765,7 +772,7 @@ let filter_all pat0 pss = let insert q qs = match q.pat_desc with | Tpat_constant ((Const_int _ | Const_char _ | Const_string _) as c) -> - find_or_add const_groups c q qs + find_or_add const_groups (const_key c) q qs | Tpat_variant (l, _, _) -> find_or_add variant_groups l q qs | _ -> ( match List.find_opt (fun (q0, _) -> simple_match q0 q) !groups with @@ -1058,21 +1065,18 @@ let build_other ext env : Typedtree.pattern = (fun p_res pat -> make_pat (Tpat_or (pat, p_res, None)) p.pat_type p.pat_env) pat other_pats) - | (({pat_desc = Tpat_constant (Const_int _)} as p), _) :: _ -> - build_other_constant - (function - | Tpat_constant (Const_int i) -> i - | _ -> assert false) - (function - | i -> Tpat_constant (Const_int i)) - 0 succ p env - | (({pat_desc = Tpat_constant (Const_char _)} as p), _) :: _ -> + | (({pat_desc = Tpat_constant ((Const_int _ | Const_char _) as c)} as p), _) + :: _ -> + (* Int and char constants can share a column; the example takes the kind + of the first one. *) build_other_constant (function - | Tpat_constant (Const_char i) -> i + | Tpat_constant (Const_int i | Const_char i) -> i | _ -> assert false) - (function - | i -> Tpat_constant (Const_char i)) + (fun i -> + match c with + | Const_char _ -> Tpat_constant (Const_char i) + | _ -> Tpat_constant (Const_int i)) 0 succ p env | (({pat_desc = Tpat_constant (Const_bigint _)} as p), _) :: _ -> build_other_constant @@ -1713,7 +1717,7 @@ let get_mins_pats pss = Tpat_constant ((Const_int _ | Const_char _ | Const_string _) as c); } :: _ -> - Some c + Some (const_key c) | _ -> None in (* [select_rec [] pss] of [get_mins le_pats]: drops each row covered by a diff --git a/compiler/ml/parmatch.mli b/compiler/ml/parmatch.mli index 1c3c50bfd5..0ae793005b 100644 --- a/compiler/ml/parmatch.mli +++ b/compiler/ml/parmatch.mli @@ -34,6 +34,9 @@ val all_record_args : (Longident.t loc * label_description * pattern * bool) list val const_compare : constant -> constant -> int +(* A hash key agreeing with [const_compare]: char constants map to ints. *) +val const_key : constant -> constant + val le_pat : pattern -> pattern -> bool val le_pats : pattern list -> pattern list -> bool diff --git a/compiler/ml/predef.ml b/compiler/ml/predef.ml index 531625ff3e..4ca9502284 100644 --- a/compiler/ml/predef.ml +++ b/compiler/ml/predef.ml @@ -434,9 +434,8 @@ let common_initial_env add_type add_extension empty_env = let build_initial_env add_type add_exception empty_env = let common = common_initial_env add_type add_exception empty_env in - let decl_type_char = - {decl_abstr with type_manifest = Some (type_int ()); type_private = Private} - in + (* [char] is an alias of [int]: a char literal is its code point. *) + let decl_type_char = {decl_abstr with type_manifest = Some (type_int ())} in add_type ident_char decl_type_char common let () = diff --git a/packages/@rescript/runtime/Primitive_string_extern.res b/packages/@rescript/runtime/Primitive_string_extern.res index f35e9f49fd..fcddf8187e 100644 --- a/packages/@rescript/runtime/Primitive_string_extern.res +++ b/packages/@rescript/runtime/Primitive_string_extern.res @@ -6,4 +6,4 @@ external length: string => int = "%string_length" -@send external getChar: (string, int) => option = "codePointAt" +@send external getChar: (string, int) => option = "codePointAt" diff --git a/tests/analysis_tests/tests/src/CompletionChar.res b/tests/analysis_tests/tests/src/CompletionChar.res new file mode 100644 index 0000000000..67675d5367 --- /dev/null +++ b/tests/analysis_tests/tests/src/CompletionChar.res @@ -0,0 +1,4 @@ +// `char` is an alias of `int`, so a char completes `Int` functions. +let c = 'a' +// let _ = c->toS +// ^com diff --git a/tests/analysis_tests/tests/src/expected/CompletionChar.res.txt b/tests/analysis_tests/tests/src/expected/CompletionChar.res.txt new file mode 100644 index 0000000000..94b273aa56 --- /dev/null +++ b/tests/analysis_tests/tests/src/expected/CompletionChar.res.txt @@ -0,0 +1,34 @@ +Complete src/CompletionChar.res 2:17 +pos_cursor:[2:17] pos_no_white:[2:16] Found expr:[2:11->2:17] +Completable: Cpath Value[c]->toS +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[c]->toS +ContextPath Value[c] +Path c +Path Stdlib.Int.toS +Path toS +[ + { + "deprecated": true, + "detail": "(int, ~radix: int) => string", + "documentation": { + "kind": "markdown", + "value": "Deprecated: Use `toString` instead\n\n\n`toStringWithRadix(n, ~radix)` return a `string` representing the given value.\n`~radix` specifies the radix base to use for the formatted number.\nSee [`Number.toString`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString)\non MDN.\n\n## Examples\n\n```rescript\nInt.toStringWithRadix(6, ~radix=2) // \"110\"\nInt.toStringWithRadix(373592855, ~radix=16) // \"16449317\"\nInt.toStringWithRadix(123456, ~radix=36) // \"2n9c\"\n```\n\n## Exceptions\n\n`RangeError`: if `radix` is less than 2 or greater than 36.\n" + }, + "kind": 12, + "label": "Int.toStringWithRadix", + "tags": [ 1 ] + }, + { + "detail": "(int, ~radix: int=?) => string", + "documentation": { + "kind": "markdown", + "value": "\n`toString(n, ~radix=?)` return a `string` representing the given value.\n`~radix` specifies the radix base to use for the formatted number.\nSee [`Number.toString`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString)\non MDN.\n\n## Examples\n\n```rescript\nInt.toString(1000) // \"1000\"\nInt.toString(-1000) // \"-1000\"\nInt.toString(6, ~radix=2) // \"110\"\nInt.toString(373592855, ~radix=16) // \"16449317\"\nInt.toString(123456, ~radix=36) // \"2n9c\"\n```\n\n## Exceptions\n\n`RangeError`: if `radix` is less than 2 or greater than 36.\n" + }, + "kind": 12, + "label": "Int.toString", + "tags": [] + } +] + diff --git a/tests/build_tests/super_errors/expected/warning_11_equivalent_char_int_patterns.res.expected b/tests/build_tests/super_errors/expected/warning_11_equivalent_char_int_patterns.res.expected new file mode 100644 index 0000000000..321987bb9d --- /dev/null +++ b/tests/build_tests/super_errors/expected/warning_11_equivalent_char_int_patterns.res.expected @@ -0,0 +1,11 @@ + + Warning number 11 + /.../fixtures/warning_11_equivalent_char_int_patterns.res:5:5-6 + + 3 ┆ switch x { + 4 ┆ | 'a' => 1 + 5 ┆ | 97 => 2 + 6 ┆ | _ => 3 + 7 ┆ } + + this match case is unused. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/warning_8_char_int_patterns.res.expected b/tests/build_tests/super_errors/expected/warning_8_char_int_patterns.res.expected new file mode 100644 index 0000000000..838e3d47fc --- /dev/null +++ b/tests/build_tests/super_errors/expected/warning_8_char_int_patterns.res.expected @@ -0,0 +1,14 @@ + + Warning number 8 + /.../fixtures/warning_8_char_int_patterns.res:3:3-6:3 + + 1 │ // Char and int patterns share one column when checking exhaustiveness. + 2 │ let f = x => + 3 │ switch x { + 4 │  | 'a' => 1 + 5 │  | 98 => 2 + 6 │  } + 7 │ + + You forgot to handle a possible case here, for example: + | '\x00' \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/warning_11_equivalent_char_int_patterns.res b/tests/build_tests/super_errors/fixtures/warning_11_equivalent_char_int_patterns.res new file mode 100644 index 0000000000..c0dcd02334 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/warning_11_equivalent_char_int_patterns.res @@ -0,0 +1,7 @@ +// `char` is an alias of `int`, so `97` repeats `'a'`. +let f = x => + switch x { + | 'a' => 1 + | 97 => 2 + | _ => 3 + } diff --git a/tests/build_tests/super_errors/fixtures/warning_8_char_int_patterns.res b/tests/build_tests/super_errors/fixtures/warning_8_char_int_patterns.res new file mode 100644 index 0000000000..501e7a1ce0 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/warning_8_char_int_patterns.res @@ -0,0 +1,6 @@ +// Char and int patterns share one column when checking exhaustiveness. +let f = x => + switch x { + | 'a' => 1 + | 98 => 2 + } diff --git a/tests/tests/src/adt_optimize_test.mjs b/tests/tests/src/adt_optimize_test.mjs index fe8b267ba0..d489fb71c5 100644 --- a/tests/tests/src/adt_optimize_test.mjs +++ b/tests/tests/src/adt_optimize_test.mjs @@ -24,9 +24,6 @@ function f_0(x) { } function f2(x) { - if (x >= 3) { - return "T003"; - } switch (x) { case 0 : return "T000"; @@ -34,6 +31,8 @@ function f2(x) { return "T001"; case 2 : return "T002"; + default: + return "T003"; } } diff --git a/tests/tests/src/char_int_alias_test.mjs b/tests/tests/src/char_int_alias_test.mjs new file mode 100644 index 0000000000..f25916d26b --- /dev/null +++ b/tests/tests/src/char_int_alias_test.mjs @@ -0,0 +1,121 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; + +let toInt = 98; + +function mixed(x) { + switch (x) { + case 97 : + return "a"; + case 98 : + return "b"; + case 99 : + case 100 : + case 101 : + return "c-e"; + case 102 : + case 103 : + return "f-g"; + default: + return "other"; + } +} + +function isDigitAt(s, i) { + let match = s.codePointAt(i); + if (match !== undefined) { + return !(match > 57 || match < 48); + } else { + return false; + } +} + +function emoji(x) { + if (x >= 128076) { + if (x >= 128513) { + if (x >= 128592) { + return "other"; + } else { + return "face"; + } + } else if (x >= 128512) { + return "grinning"; + } else { + return "other"; + } + } else if (x !== 55357) { + if (x >= 128075) { + return "wave"; + } else { + return "other"; + } + } else { + return "high surrogate"; + } +} + +function emojiAt(s, i) { + let c = s.codePointAt(i); + if (c !== undefined) { + return emoji(c); + } else { + return "none"; + } +} + +function isControl(c) { + return !(c > 31 || c < 0); +} + +Mocha.describe("Char_int_alias_test", () => { + Mocha.test("conversions", () => { + Test_utils.eq("File \"char_int_alias_test.res\", line 50, characters 7-14", 65, /* 'A' */65); + Test_utils.eq("File \"char_int_alias_test.res\", line 51, characters 7-14", toInt, 98); + }); + Mocha.test("mixed patterns", () => { + Test_utils.eq("File \"char_int_alias_test.res\", line 55, characters 7-14", "a", "a"); + Test_utils.eq("File \"char_int_alias_test.res\", line 56, characters 7-14", mixed(/* 'b' */98), "b"); + Test_utils.eq("File \"char_int_alias_test.res\", line 57, characters 7-14", mixed(/* 'd' */100), "c-e"); + Test_utils.eq("File \"char_int_alias_test.res\", line 58, characters 7-14", mixed(/* 'f' */102), "f-g"); + Test_utils.eq("File \"char_int_alias_test.res\", line 59, characters 7-14", "f-g", "f-g"); + Test_utils.eq("File \"char_int_alias_test.res\", line 60, characters 7-14", "other", "other"); + Test_utils.eq("File \"char_int_alias_test.res\", line 61, characters 7-14", "other", "other"); + }); + Mocha.test("code points", () => { + Test_utils.eq("File \"char_int_alias_test.res\", line 65, characters 7-14", isDigitAt("a1", 0), false); + Test_utils.eq("File \"char_int_alias_test.res\", line 66, characters 7-14", isDigitAt("a1", 1), true); + Test_utils.eq("File \"char_int_alias_test.res\", line 67, characters 7-14", isDigitAt("a1", 2), false); + }); + Mocha.test("emoji", () => { + Test_utils.eq("File \"char_int_alias_test.res\", line 71, characters 7-14", /* '😀' */128512, 128512); + Test_utils.eq("File \"char_int_alias_test.res\", line 72, characters 7-14", "😀".codePointAt(0), /* '😀' */128512); + Test_utils.eq("File \"char_int_alias_test.res\", line 73, characters 7-14", emoji(/* '😀' */128512), "grinning"); + Test_utils.eq("File \"char_int_alias_test.res\", line 74, characters 7-14", emoji(/* '😂' */128514), "face"); + Test_utils.eq("File \"char_int_alias_test.res\", line 75, characters 7-14", emoji(/* '👋' */128075), "wave"); + Test_utils.eq("File \"char_int_alias_test.res\", line 76, characters 7-14", emoji(/* '🚀' */128640), "other"); + Test_utils.eq("File \"char_int_alias_test.res\", line 77, characters 7-14", emojiAt("a😀", 1), "grinning"); + Test_utils.eq("File \"char_int_alias_test.res\", line 80, characters 7-14", emojiAt("a😀", 2), "other"); + Test_utils.eq("File \"char_int_alias_test.res\", line 81, characters 7-14", emojiAt(String.fromCharCode(55357), 0), "high surrogate"); + Test_utils.eq("File \"char_int_alias_test.res\", line 82, characters 7-14", emojiAt("a😀", 3), "none"); + }); + Mocha.test("negative values", () => { + Test_utils.eq("File \"char_int_alias_test.res\", line 86, characters 7-14", true, true); + Test_utils.eq("File \"char_int_alias_test.res\", line 87, characters 7-14", false, false); + Test_utils.eq("File \"char_int_alias_test.res\", line 88, characters 7-14", false, false); + }); +}); + +let fromInt = 65; + +export { + fromInt, + toInt, + mixed, + isDigitAt, + emoji, + emojiAt, + isControl, +} +/* Not a pure module */ diff --git a/tests/tests/src/char_int_alias_test.res b/tests/tests/src/char_int_alias_test.res new file mode 100644 index 0000000000..c7c1d7454f --- /dev/null +++ b/tests/tests/src/char_int_alias_test.res @@ -0,0 +1,90 @@ +open Mocha +open Test_utils + +// `char` is an alias of `int`, so char literals and ints mix freely. +let fromInt: char = 65 +let toInt: int = 'a' + 1 + +let mixed = x => + switch x { + | 'a' => "a" + | 98 => "b" + | 'c' .. 'e' => "c-e" + | 102 | 'g' => "f-g" + | _ => "other" + } + +// A char range matches the int result of `codePointAt` directly. +let isDigitAt = (s, i) => + switch String.codePointAt(s, i) { + | Some('0' .. '9') => true + | Some(_) | None => false + } + +// Emoji are outside the BMP: a literal is the full code point, as returned by +// `codePointAt`, not a UTF-16 surrogate. +let emoji = x => + switch x { + | '😀' => "grinning" + | '\u{1F601}' .. '\u{1F64F}' => "face" + | 0x1F44B => "wave" + | 0xD83D => "high surrogate" + | _ => "other" + } + +let emojiAt = (s, i) => + switch String.codePointAt(s, i) { + | Some(c) => emoji(c) + | None => "none" + } + +// A char can now hold any int, so the lower bound must be checked. +let isControl = (c: char) => + switch c { + | '\000' .. '\031' => true + | _ => false + } + +describe(__MODULE__, () => { + test("conversions", () => { + eq(__LOC__, fromInt, 'A') + eq(__LOC__, toInt, 98) + }) + + test("mixed patterns", () => { + eq(__LOC__, mixed(97), "a") + eq(__LOC__, mixed('b'), "b") + eq(__LOC__, mixed('d'), "c-e") + eq(__LOC__, mixed('f'), "f-g") + eq(__LOC__, mixed(103), "f-g") + eq(__LOC__, mixed(104), "other") + eq(__LOC__, mixed(-97), "other") + }) + + test("code points", () => { + eq(__LOC__, isDigitAt("a1", 0), false) + eq(__LOC__, isDigitAt("a1", 1), true) + eq(__LOC__, isDigitAt("a1", 2), false) + }) + + test("emoji", () => { + eq(__LOC__, '😀', 0x1F600) + eq(__LOC__, String.codePointAt("😀", 0), Some('😀')) + eq(__LOC__, emoji('😀'), "grinning") + eq(__LOC__, emoji('😂'), "face") + eq(__LOC__, emoji('👋'), "wave") + eq(__LOC__, emoji('🚀'), "other") + eq(__LOC__, emojiAt("a😀", 1), "grinning") + // Index 2 is the low surrogate of 😀. A lone high surrogate is its own + // code point. + eq(__LOC__, emojiAt("a😀", 2), "other") + eq(__LOC__, emojiAt(String.fromCharCode(0xD83D), 0), "high surrogate") + eq(__LOC__, emojiAt("a😀", 3), "none") + }) + + test("negative values", () => { + eq(__LOC__, isControl('\n'), true) + eq(__LOC__, isControl(' '), false) + eq(__LOC__, isControl(-1), false) + }) +}) diff --git a/tests/tests/src/string_unicode_test.mjs b/tests/tests/src/string_unicode_test.mjs index c0fc6c432d..969ad27975 100644 --- a/tests/tests/src/string_unicode_test.mjs +++ b/tests/tests/src/string_unicode_test.mjs @@ -10,14 +10,16 @@ console.log(`你好`); console.log("你好"); function f(x) { - if (x !== 333) { - if (x >= 256) { + if (x >= 256) { + if (x !== 333) { return 3; } else { - return 0; + return 2; } + } else if (x >= 0) { + return 0; } else { - return 2; + return 3; } }