Semantic Diff

#8894 Support int range patterns

5 commits, rendered from their records.

Summary

RangeCommitsChangelogAreasJS output changesFormatsFailed checks
06cc086c2324008d907df13562671457a89640f1..pr-88945๐Ÿš€ New feature ร—2 ๐Ÿ’… Polishtypecheckerlambdaanalysistestsruntime1cmt (Caml1999T045 -> Caml1999T046)1

Changelog draft

๐Ÿš€ New feature
  • char is now an alias of int, so char literals and ints mix freely, including in switch patterns.
  • Int range patterns such as 200 .. 299 are supported in switch, and their bounds may mix ints and chars.
๐Ÿ’… Polish
  • Char range patterns such as '\u{0}' .. '\u{10FFFF}' compile as fast as a single character, and missing-case warnings show ranges instead of listing every character.

Files changed by more than one commit

compiler/ml/matching.ml1df38d69b, a4a97df68
compiler/ml/parmatch.ml1df38d69b, a4a97df68, 9a4e37f91, 1bd580c39
compiler/ml/parmatch.mli1df38d69b, a4a97df68
compiler/ml/typecore.mla4a97df68, 9a4e37f91
tests/ERROR_VARIANTS.md1df38d69b, a4a97df68, 9a4e37f91, 1bd580c39

06cc086c2324008d907df13562671457a89640f1..pr-8894

1df38d69b ยท Christoph Knittel ยท Oct 18, 2026

Make char an alias of int

๐Ÿš€ New featuretypecheckerlambdaanalysistests

char was a private abbreviation of int: a char could be coerced to an int with :>, but an int could not be used as a char. It is now a plain alias, so char literals and ints mix freely, including in one switch. Pattern matching treats a char constant and an int constant with the same value as the same constant, for exhaustiveness, unused cases and switch compilation. Char literals keep their spelling in generated JavaScript comments and in warning examples. First step of #7028.

Example ReScript

let f = x =>  switch x {  | '\000' .. '\255' => 0  | 'ล' => 2  | _ => 3  }
Before JavaScript
function f(x) {  if (x !== 333) {    if (x >= 256) {      return 3;    } else {      return 0;    }  } else {    return 2;  }}
After JavaScript
function f(x) {  if (x >= 256) {    if (x !== 333) {      return 3;    } else {      return 2;    }  } else if (x >= 0) {    return 0;  } else {    return 3;  }}

Source tests/tests/src/string_unicode_test.mjs in the diff (the range starts at code point 0, written as a literal NUL character in the test).

๐Ÿš€ New featurechar is now an alias of int, so char literals and ints mix freely, including in switch patterns.

Review

No effect
Formatting: unchangedFormats: nonePPX: unchanged
Output
changedtests/tests/src/adt_optimize_test.mjs and string_unicode_test.mjs, char switches whose range starts at '\000' now send negative values to the default case
Editor
pipecompletion on a char value offers Int functions; it offered none before (tests/analysis_tests/tests/src/CompletionChar.res)
Errors
ERROR_VARIANTS.md warnings section lists the two new warning 8 and 11 fixtures
Tests
integration tests/tests/src/+ char_int_alias_test.res~ adt_optimize_test.mjs~ string_unicode_test.mjs
analysis+ tests/analysis_tests/tests/src/CompletionChar.res
super_errors tests/build_tests/+ super_errors/fixtures/warning_8_char_int_patterns.res+ super_errors/fixtures/warning_11_equivalent_char_int_patterns.res
17 files ยท +346 โˆ’51 ยท analysis 1 ยท changelog 1 ยท typechecker 4 ยท tests 11
Manalysis/src/type_utils.ml+4โˆ’2
Achanges/char-int-alias.feature.md+1โˆ’0
Mcompiler/ml/matching.ml+17โˆ’22
Mcompiler/ml/parmatch.ml+20โˆ’16
Mcompiler/ml/parmatch.mli+3โˆ’0
Mcompiler/ml/predef.ml+2โˆ’3
Mtests/ERROR_VARIANTS.md+4โˆ’1
Atests/analysis_tests/tests/src/CompletionChar.res+4โˆ’0
Atests/analysis_tests/tests/src/expected/CompletionChar.res.txt+34โˆ’0
Atests/build_tests/super_errors/expected/warning_11_equivalent_char_int_patterns.res.expected+11โˆ’0
Atests/build_tests/super_errors/expected/warning_8_char_int_patterns.res.expected+14โˆ’0
Atests/build_tests/super_errors/fixtures/warning_11_equivalent_char_int_patterns.res+7โˆ’0
Atests/build_tests/super_errors/fixtures/warning_8_char_int_patterns.res+6โˆ’0
Mtests/tests/src/adt_optimize_test.mjs+2โˆ’3
Atests/tests/src/char_int_alias_test.mjs+121โˆ’0
Atests/tests/src/char_int_alias_test.res+90โˆ’0
Mtests/tests/src/string_unicode_test.mjs+6โˆ’4
with Claude Opus 5.5
a4a97df68 ยท Christoph Knittel ยท Oct 18, 2026

Represent char range patterns with a typed-tree node

๐Ÿ’… Polishtypecheckerlambdaanalysistests

A char range pattern such as 'a' .. 'z' used to be expanded by the type checker into an or-pattern with one constant per character, so compile time and memory grew with the width of the range. It is now one Tpat_interval node. Exhaustiveness and match compilation split each column at the bounds of its ranges into pieces that every range covers fully or not at all, so the work depends on the number of patterns, not on the number of characters. Missing-case warnings show ranges instead of listing every character.

Example ReScript

let f = (a, b) =>  switch (a, b) {  | ('a' .. 'z', 'a' .. 'z') => 1  | (_, '\u{0}' .. '`') => 2  }
Before output
You forgot to handle a possible case here, for example:  | ('t', '{')| ('u', '{')| ('v', '{')| ('w', '{')| ('x', '{')| ('y', '{')| ('z', '{')
After output
You forgot to handle a possible case here, for example:  | ('a' .. 'z', '{') | ('\x00', 'a')

Source tests/build_tests/super_errors/fixtures/warning_8_char_range_example.res, compiled with the parent commit and with this one.

๐Ÿ’… PolishChar range patterns such as '\u{0}' .. '\u{10FFFF}' compile as fast as a single character, and missing-case warnings show ranges instead of listing every character.

Review

No effect
Formatting: unchanged
Output
unchangedall committed generated JS is byte-identical (char_range_test.mjs changes with its source)
Formats
cmt(Caml1999T045 -> Caml1999T046)
PPX
unchangedPpat_interval already exists in the v0 parsetree
Editor
unchanged;analysis visits the new node like a constant
Errors
ERROR_VARIANTS.md warnings section lists the two new fixtures
Tests
integration tests/tests/src/~ char_range_test.res
super_errors tests/build_tests/+ super_errors/fixtures/warning_8_char_range_example.res+ super_errors/fixtures/warning_11_char_range_covered.res
Measured
  • switch on '\u{0}' .. '\u{10FFFF}': 15.75 s and 1.78 GB -> 0.02 s and 19 MB (bsc wall time and max RSS, /usr/bin/time, parent commit vs this one)
  • switch on '\u{0}' .. '\u{FFFF}': 0.55 s and 135 MB โ†’ 0.02 s and 20 MB
23 files ยท +680 โˆ’143 ยท analysis 1 ยท changelog 1 ยท compiler 2 ยท typechecker 12 ยท tests 7
Manalysis/src/process_cmt.ml+3โˆ’1
Achanges/char-range-patterns.polish.md+1โˆ’0
Mcompiler/common/pattern_printer.ml+5โˆ’3
Mcompiler/ext/config.ml+1โˆ’1
Mcompiler/ml/dict_pattern.ml+3โˆ’1
Mcompiler/ml/matching.ml+203โˆ’67
Mcompiler/ml/parmatch.ml+161โˆ’29
Mcompiler/ml/parmatch.mli+7โˆ’0
Mcompiler/ml/printtyped.ml+2โˆ’0
Mcompiler/ml/rec_check.ml+2โˆ’2
Mcompiler/ml/tast_iterator.ml+1โˆ’1
Mcompiler/ml/tast_mapper.ml+1โˆ’1
Mcompiler/ml/typecore.ml+13โˆ’15
Mcompiler/ml/typedtree.ml+5โˆ’2
Mcompiler/ml/typedtree.mli+4โˆ’0
Mcompiler/ml/typedtree_iter.ml+1โˆ’1
Mtests/ERROR_VARIANTS.md+5โˆ’4
Atests/build_tests/super_errors/expected/warning_11_char_range_covered.res.expected+11โˆ’0
Atests/build_tests/super_errors/expected/warning_8_char_range_example.res.expected+14โˆ’0
Atests/build_tests/super_errors/fixtures/warning_11_char_range_covered.res+8โˆ’0
Atests/build_tests/super_errors/fixtures/warning_8_char_range_example.res+6โˆ’0
Mtests/tests/src/char_range_test.mjs+142โˆ’13
Mtests/tests/src/char_range_test.res+81โˆ’2
with Claude Opus 5.5
9a4e37f91 ยท Christoph Knittel ยท Oct 18, 2026

Support int range patterns

๐Ÿš€ New featuretypecheckertests

switch now accepts int ranges such as 200 .. 299, and bounds that mix ints and chars, since char is an alias of int. They use the Tpat_interval node char ranges compile to, so a range costs the same however wide it is. Ranges of other constants, such as 1.0 .. 5.0, still report Invalid_interval, whose message now names int and char intervals. Int switches are never exhaustive, as before: a missing-case example is the least uncovered value from 0 up, else the greatest one below 0.

Example ReScript

let status = code =>  switch code {  | 200 .. 299 => "success"  | 301 | 302 | 307 | 308 => "redirect"  | 400 .. 499 => "client error"  | 500 .. 599 => "server error"  | _ => "unknown"  }

After JavaScript

function status(code) {  if (code >= 309) {    if (code >= 500) {      if (code >= 600) {        return "unknown";      } else {        return "server error";      }    } else if (code >= 400) {      return "client error";    } else {      return "unknown";    }  }  if (code < 200) {    return "unknown";  }  if (code < 300) {    return "success";  }  switch (code) {    case 300 :    case 303 :    case 304 :    case 305 :    case 306 :      return "unknown";    case 301 :    case 302 :    case 307 :    case 308 :      return "redirect";  }}

Source tests/tests/src/int_range_test.mjs in the diff. Before this commit the example reported "Only character intervals are supported in patterns."

๐Ÿš€ New featureInt range patterns such as 200 .. 299 are supported in switch, and their bounds may mix ints and chars.

Review

No effect
Formatting: unchangedFormats: nonePPX: unchangedEditor: unchanged
Output
unchangedexisting programs compile the same; int ranges are new
Errors
Invalid_interval relinked to pattern_interval_float.res (was pattern_interval_non_char.res); warnings section lists warning_8_int_range_example.res
Tests
integration tests/tests/src/+ int_range_test.res
super_errors tests/build_tests/+ super_errors/fixtures/warning_8_int_range_example.res+ super_errors/fixtures/pattern_interval_float.resโˆ’ super_errors/fixtures/pattern_interval_non_char.res
11 files ยท +333 โˆ’42 ยท changelog 1 ยท typechecker 2 ยท tests 8
Achanges/int-range-patterns.feature.md+1โˆ’0
Mcompiler/ml/parmatch.ml+21โˆ’10
Mcompiler/ml/typecore.ml+30โˆ’14
Mtests/ERROR_VARIANTS.md+7โˆ’6
Atests/build_tests/super_errors/expected/pattern_interval_float.res.expected+11โˆ’0
Dtests/build_tests/super_errors/expected/pattern_interval_non_char.res.expected+0โˆ’11
Atests/build_tests/super_errors/expected/warning_8_int_range_example.res.expected+14โˆ’0
Rtests/build_tests/super_errors/fixtures/pattern_interval_float.res+1โˆ’1
Atests/build_tests/super_errors/fixtures/warning_8_int_range_example.res+6โˆ’0
Atests/tests/src/int_range_test.mjs+155โˆ’0
Atests/tests/src/int_range_test.res+87โˆ’0
with Claude Opus 5.5
1bd580c39 ยท Christoph Knittel ยท Oct 18, 2026

Write missing-case examples beyond char literals as ints

typecheckertests

A missing-case example in a char column took the kind of the first pattern, so a value that no char literal can denote was printed as an invalid one: a switch covering every char literal suggested '\u{110000}'. Such values are now printed as ints.

Example ReScript

let f = c =>  switch c {  | '\u{0}' .. '\u{10FFFF}' => 1  }
Before output
You forgot to handle a possible case here, for example:  | '\u{110000}'
After output
You forgot to handle a possible case here, for example:  | 1114112

Source tests/build_tests/super_errors/fixtures/warning_8_char_range_beyond.res, compiled with the parent commit and with this one.

Review

No effect
Output: unchanged
Errors
ERROR_VARIANTS.md warnings section lists warning_8_char_range_beyond.res
Tests
super_errors tests/build_tests/+ super_errors/fixtures/warning_8_char_range_beyond.res
4 files ยท +28 โˆ’6 ยท typechecker 1 ยท tests 3
Mcompiler/ml/parmatch.ml+8โˆ’5
Mtests/ERROR_VARIANTS.md+2โˆ’1
Atests/build_tests/super_errors/expected/warning_8_char_range_beyond.res.expected+13โˆ’0
Atests/build_tests/super_errors/fixtures/warning_8_char_range_beyond.res+5โˆ’0
with Claude Opus 5.5
351960ad6 ยท Christoph Knittel ยท Oct 18, 2026

Type the runtime's code point lookup as int

runtime

Primitive_string_extern.getChar returned option<char>; it returns option<int> now that char is an alias of int, which leaves no use of char in the runtime. The generated JavaScript is unchanged.

No effect
Output: unchanged
Tests
nonebecause the type is an alias of the old one and the output is unchanged
1 files ยท +1 โˆ’1 ยท runtime 1
Mpackages/@rescript/runtime/Primitive_string_extern.res+1โˆ’1
with Claude Opus 5.5