Skip to content

Support int range patterns - #8894

Merged
cknitt merged 1 commit into
char-intervalsfrom
char-int-ranges
Oct 19, 2026
Merged

cknitt merged 1 commit into
char-intervalsfrom
char-int-ranges

Conversation

@cknitt

@cknitt cknitt commented Oct 18, 2026

Copy link
Copy Markdown
Member

Third step of #7028, stacked on #8893.

switch now accepts int range patterns, and their bounds may mix ints and chars, since char is an alias of int (#8892). They use the Tpat_interval node from #8893, so a range costs the same however wide it is.

let status = code =>
  switch code {
  | 200 .. 299 => "success"
  | 301 | 302 | 307 | 308 => "redirect"
  | 400 .. 499 => "client error"
  | 500 .. 599 => "server error"
  | _ => "unknown"
  }
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";
  }
}
  • Type checker: accepts int and mixed bounds; reversed bounds are swapped, as for chars. Ranges of other constants (1.0 .. 5.0) still report Invalid_interval, whose message now reads "Only int and char intervals are supported in patterns."
  • Exhaustiveness: int switches are never exhaustive, as before. The missing-case example is the least uncovered value from 0 up, else the greatest one below 0, or _ if all of int32 is covered.

New tests:

  • int_range_test.res: negative, hex, reversed and mixed bounds, large ranges, HTTP status codes, ranges in several columns.
  • super_errors: a missing-case example below 0 (warning 8), and Invalid_interval with float bounds. That fixture replaces pattern_interval_non_char.res, since 1 .. 5 is now valid.

A differential fuzzer ran 600 switches over int, char and mixed ranges (negative bounds, or-patterns, guards, tuples, options). For every input, the results equal a reference evaluation. Three programs hit the pre-existing Matching.comp_exit crash described in #8893.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JALXUyHhkDqzcB3WuW9Svo

@cknitt
cknitt added this pull request to stack #8895 October 18, 2026 09:55
@pkg-pr-new

pkg-pr-new Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript@8894

@rescript/belt

npm i https://pkg.pr.new/@rescript/belt@8894

@rescript/darwin-arm64

npm i https://pkg.pr.new/@rescript/darwin-arm64@8894

@rescript/darwin-x64

npm i https://pkg.pr.new/@rescript/darwin-x64@8894

@rescript/linux-arm64

npm i https://pkg.pr.new/@rescript/linux-arm64@8894

@rescript/linux-x64

npm i https://pkg.pr.new/@rescript/linux-x64@8894

@rescript/runtime

npm i https://pkg.pr.new/@rescript/runtime@8894

@rescript/win32-x64

npm i https://pkg.pr.new/@rescript/win32-x64@8894

commit: 24a773a

@cknitt

cknitt commented Oct 18, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-18T11:51:56.819617Z 4d2e390 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a4e37f913

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread compiler/ml/typecore.ml
cknitt added a commit that referenced this pull request Oct 18, 2026
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 (text):
    You forgot to handle a possible case here, for example:
      | '\u{110000}'

After (text):
    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: Codex found this for mixed ranges on #8894, where an example below 0
was printed as a char; the cause is here.

Area: typechecker tests
Output: unchanged
Errors: ERROR_VARIANTS.md warnings section lists warning_8_char_range_beyond.res
Tests: +tests/build_tests/super_errors/fixtures/warning_8_char_range_beyond.res
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JALXUyHhkDqzcB3WuW9Svo
Signed-off-by: Christoph Knittel <ck@cca.io>
@codecov

codecov Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.46154% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.04%. Comparing base (02d5ac2) to head (24a773a).

Files with missing lines Patch % Lines
compiler/ml/typecore.ml 83.33% 2 Missing ⚠️
compiler/ml/parmatch.ml 92.85% 1 Missing ⚠️
Additional details and impacted files
@@               Coverage Diff               @@
##           char-intervals    #8894   +/-   ##
===============================================
  Coverage           81.04%   81.04%           
===============================================
  Files                 455      455           
  Lines               63660    63675   +15     
===============================================
+ Hits                51591    51605   +14     
- Misses              12069    12070    +1     
Files with missing lines Coverage Δ
compiler/ml/parmatch.ml 77.26% <92.85%> (+0.17%) ⬆️
compiler/ml/typecore.ml 86.59% <83.33%> (-0.01%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cknitt

cknitt commented Oct 18, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 4d2e390417

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cknitt
cknitt requested a review from cristianoc October 18, 2026 12:05
@cknitt
cknitt marked this pull request as ready for review October 18, 2026 12:05

@cristianoc cristianoc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legit

cknitt added a commit that referenced this pull request Oct 18, 2026
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 (text):
    You forgot to handle a possible case here, for example:
      | '\u{110000}'

After (text):
    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: Codex found this for mixed ranges on #8894, where an example below 0
was printed as a char; the cause is here.

Area: typechecker tests
Output: unchanged
Errors: ERROR_VARIANTS.md warnings section lists warning_8_char_range_beyond.res
Tests: +tests/build_tests/super_errors/fixtures/warning_8_char_range_beyond.res
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JALXUyHhkDqzcB3WuW9Svo
Signed-off-by: Christoph Knittel <ck@cca.io>
cknitt added a commit that referenced this pull request Oct 18, 2026
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 (text):
    You forgot to handle a possible case here, for example:
      | '\u{110000}'

After (text):
    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: Codex found this for mixed ranges on #8894, where an example below 0
was printed as a char; the cause is here.

Area: typechecker tests
Output: unchanged
Errors: ERROR_VARIANTS.md warnings section lists warning_8_char_range_beyond.res
Tests: +tests/build_tests/super_errors/fixtures/warning_8_char_range_beyond.res
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JALXUyHhkDqzcB3WuW9Svo
Signed-off-by: Christoph Knittel <ck@cca.io>
`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 (js):
    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."

Review: Does a switch covering all ints count as exhaustive? No. Ints are
never matched exhaustively, so warning 8 still reports `_` for a switch on
`-0x80000000 .. -1 | 0 .. 0x7FFFFFFF`, as it does for constants.

Review: Can a mixed range give an invalid example? Codex found that a range
from a char to an int covering all non-negative ints suggested a char for
-1. Examples are written as chars only when they are valid code points, a
fix in the parent commit; warning_8_mixed_range_example.res covers this case.

Review: How was it checked? A fuzzer generated 600 switches over int, char and
mixed ranges with negative bounds, or-patterns, guards, tuples and options;
the results equal a reference evaluation for every input. Three programs hit
`Fatal error: Matching.comp_exit`; each reduces to `| _ => ... | p1 | p2 if
g() => ...`, which crashes the parent commits the same way with char
patterns.

Area: typechecker tests
Output: unchanged, existing programs compile the same; int ranges are new
Formatting: unchanged
Formats: none
PPX: unchanged
Editor: unchanged
Tests: +tests/tests/src/int_range_test.res
 +tests/build_tests/super_errors/fixtures/warning_8_int_range_example.res
 +tests/build_tests/super_errors/fixtures/warning_8_mixed_range_example.res
 +tests/build_tests/super_errors/fixtures/pattern_interval_float.res
 -tests/build_tests/super_errors/fixtures/pattern_interval_non_char.res
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JALXUyHhkDqzcB3WuW9Svo
Signed-off-by: Christoph Knittel <ck@cca.io>
@github-actions

Copy link
Copy Markdown

@cknitt
cknitt merged commit 407c384 into master Oct 19, 2026
34 of 50 checks passed
@cknitt
cknitt deleted the char-int-ranges branch October 19, 2026 07:15
cknitt added a commit that referenced this pull request Oct 19, 2026
* Represent char range patterns with a typed-tree node

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 (text):
    You forgot to handle a possible case here, for example:
      | ('t', '{')
    | ('u', '{')
    | ('v', '{')
    | ('w', '{')
    | ('x', '{')
    | ('y', '{')
    | ('z', '{')

After (text):
    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.

Review: Is the generated JavaScript the same? Every committed snapshot is
byte-identical. A differential fuzzer (900 random switches over overlapping
char ranges, or-patterns, guards, tuples and options) compiled each program
with the parent commit and with this one: results equal a reference
evaluation for every input, warning numbers and locations are equal, and the
JS differs in shape for some programs and is 0.4% smaller in total
(296,645 -> 295,495 characters).

Review: Why are intervals precompiled like or-patterns? An expanded range
used to be an or-pattern, whose row is compiled once in a handler. Doing the
same for an interval whose row tests further columns keeps that sharing;
dividing such rows by piece instead made the fuzzer's output 16% larger.

Review: Why is the action of a range stored as shared? The switch emits it
for each value of the range. An expanded range shared it the same way,
because its values had equal actions; a copy per value would declare the
action's variables twice in one JavaScript scope.

Review: Are int ranges such as `1 .. 5` accepted? Not yet; they still report
`Invalid_interval`. The node and the algorithms already work on int and
char constants, for a follow-up.

Area: typechecker lambda analysis tests
Output: unchanged, all committed generated JS is byte-identical
 (char_range_test.mjs changes with its source)
Formatting: unchanged
Formats: cmt (Caml1999T045 -> Caml1999T046)
PPX: unchanged, Ppat_interval already exists in the v0 parsetree
Editor: unchanged; analysis visits the new node like a constant
Tests: ~tests/tests/src/char_range_test.res
 +tests/build_tests/super_errors/fixtures/warning_8_char_range_example.res
 +tests/build_tests/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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JALXUyHhkDqzcB3WuW9Svo
Signed-off-by: Christoph Knittel <ck@cca.io>

* Write missing-case examples beyond char literals as ints

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 (text):
    You forgot to handle a possible case here, for example:
      | '\u{110000}'

After (text):
    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: Codex found this for mixed ranges on #8894, where an example below 0
was printed as a char; the cause is here.

Area: typechecker tests
Output: unchanged
Tests: +tests/build_tests/super_errors/fixtures/warning_8_char_range_beyond.res
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JALXUyHhkDqzcB3WuW9Svo
Signed-off-by: Christoph Knittel <ck@cca.io>

---------

Signed-off-by: Christoph Knittel <ck@cca.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants