You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Note: Char doesn't support Unicode or UTF-8 and is therefore not recommended.
The char doesn't support Unicode, but only supports UTF-16 codepoint.
leta= '👋'
compiles to
leta=128075;
Its value is the same as '👋'.codePointAt(0) result in JavaScript, which means that in the value representation, char is equivalent to int (16-bit subset).
Then, why don't we use just int instead of char?
char literals are automatically compiled to codepoints. This is much more efficient than string representation when dealing with the Unicode data table.
char supports range pattern (e.g. 'a' .. 'z') in pattern matching. This is very useful when writing parsers.
However, a char literal is not really useful to represent a Unicode character because it doesn't cover the entire Unicode sequence. It only returns the first codepoint value and discards the rest of the character segment.
To avoid problems, we should limit the value range of char literal to the BMP(Basic Multilingual Plane, U+0000~U+FFFF).
Suggestion
I suggest some changes that would keep the useful parts of char but remove its confusion.
Get rid of char type or make it an alias of int
Keep char literal syntax, but with internal representation as regular integers
Limit the char literal range to BMP in the syntax level.
Looks good in principle. Just wondering about what kind of functionality one would want.
Any way to use former chars, now int, to build things, as opposed to just taking things apart?
And how do we guarantee they are in the BMP range at that point?
The char type was originally used to interact with the String module from OCaml, but this becomes problematic when supporting a wider range than OCaml's.
In the current version:
// '𐀀' = 0x10000let_=String.get("𐀀", n) // behave like Js's String.prototype.codePointAtlet_=String.make(10, '𐀀')
// ^ This has unexpected value "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"// Wrong on both Js and OCaml side
The proposal "fixes" current behavior while not breaking it. After removing the OCaml modules in #6984, I expect it to be unused, but it shouldn't cause any problems even in the code that is still using it.
BMP is a boundary that a UTF-16 character appears as a surrogate pair. If the codepoint is in the BMP, it can be represented in JS as a string with length=1 and it's safe to treat a string as a sequence of chars.
One observation is that the internal representation as integers might have to wait until after parsing, as pretty printing will restore the original.
Unless pretty printing itself will do some sort of normalization (when converting back from int).
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Hey — just wanted to drop some real-world signal on this RFC from a large-ish codebase migration.
I maintain IDApTIK, a ~542 .res file browser game (PixiJS, reversible VM, multiplayer sync). We've been running a structured v12 → v13 migration test on a staging fork, and the char/Unicode situation was directly relevant.
What we hit: On Linux (Fedora, UTF-8 locale), rescript.exe would crash on our codebase — we had to use rescript-legacy as a workaround for the entire v12 cycle. We were specifically testing in our v13 migration plan (Phase 4) whether the parser rewrite to Rust (#8239) resolved this. I suspect it did — the Rust parser handles encoding differently from the OCaml one — but I haven't been able to confirm yet as v13 testing is still in progress.
What I was working on: I had a root cause analysis and fix plan drafted for the UTF-8 crash, covering the encoding assumptions in the compiler binary. The parser rewrite likely made that moot for the crash specifically, but the underlying char type semantics issue this RFC addresses is still very much present.
Supporting the RFC from practical experience:
The char → UTF-16 codepoint silent truncation is a real footgun. In a game with i18n support (we have A2ML-based localisation), you encounter non-BMP characters regularly. The current behaviour of silently discarding everything past the first codepoint is genuinely dangerous.
The Stdlib_Char removal (remove Stdlib_Char module #7367) was a good step. We migrated off Char module usage without issues.
Integer range patterns (48..57) would be very welcome — we have several places in our VM instruction decoder where we pattern match on character codes and currently have to use guard clauses.
Basically: looks like the parser rewrite may have sorted our specific crash (great work on that!), but this RFC's core proposal — making char an alias of int, restricting literals to BMP, and enabling int range patterns — would still be a meaningful improvement. Happy to provide migration testing data from our codebase if that's useful for scoping the implementation.
Char literals no longer silently drop extra code points. A literal must be exactly one code point, so '👋🏽' (hand + skin-tone modifier) is now a syntax error instead of being truncated to '👋'.
The literal's original source text is kept alongside its integer value (Pconst_char {source; semantic}), so the formatter prints literals back exactly as they were written.
The String.make(10, '𐀀') problem went away with the OCaml String module.
What's left, and my proposal:
Make char an alias of int. Chars are already plain integers at runtime, and the separate type mostly forces Obj.magic when you want to do arithmetic on code points. Existing code that type-checks keeps working. Literal syntax stays.
Don't limit literals to the BMP. I'd keep the full Unicode range. Reading characters goes through codePointAt, which returns full code points ('😀' → 0x1F600). With a BMP limit, switch String.codePointAt(s, i) { | Some('😀') => ... } couldn't be written. The limit would only make sense if we modelled UTF-16 code units (charCodeAt), and we don't.
Integer range patterns (48 .. 57). Yes, but this needs real work. Today 'a' .. 'z' is expanded into an or-pattern with one case per value, which doesn't scale to arbitrary int ranges. Doing this properly means intervals in the typed tree, interval-aware exhaustiveness checks, and switch compilation that emits x >= a && x <= b. I'd track this as its own issue. Char ranges would then use the same mechanism.
I suggest doing (1) as a small PR and opening a separate issue for (3). We can then close this RFC.
Thanks Christoph, this clears things up for me. Keeping full code points makes sense; I’d been conflating that with the problem of literals silently losing extra characters in my earlier comment.
I’ve also seen the PRs since this reply. Having char literals work directly with ints, along with integer ranges, looks like it would cover what I was hoping for on the IDApTIK side.
This is now implemented; all parts landed on master:
char is an alias of int (Make char an alias of int #8892). Char literals and ints mix freely, including in one switch, and the result of String.codePointAt (option<int>) can be matched against char patterns directly:
No BMP limit. As proposed above, char literals keep the full Unicode range, since codePointAt returns full code points.
Range patterns are a single pattern (Represent char range patterns with a typed-tree node #8893). A range used to be expanded into one case per character; now it compiles at the same speed however wide it is. A switch on '\u{0}' .. '\u{10FFFF}' takes 0.02 s instead of about 16 s, and missing-case warnings show ranges instead of listing characters.
Activity
zth commentedon Sep 8, 2024
I like it! "Support range patterns for regular integers" especially. But I'm not very read up on these things.
cristianoc commentedon Sep 8, 2024
Looks good in principle. Just wondering about what kind of functionality one would want.
Any way to use former chars, now int, to build things, as opposed to just taking things apart?
And how do we guarantee they are in the BMP range at that point?
cristianoc commentedon Sep 8, 2024
If ints, formerly char, are not used for building things, then what else should be used? Always strings?
cometkim commentedon Sep 8, 2024
The
chartype was originally used to interact with theStringmodule from OCaml, but this becomes problematic when supporting a wider range than OCaml's.In the current version:
The proposal "fixes" current behavior while not breaking it. After removing the OCaml modules in #6984, I expect it to be unused, but it shouldn't cause any problems even in the code that is still using it.
BMP is a boundary that a UTF-16 character appears as a surrogate pair. If the codepoint is in the BMP, it can be represented in JS as a string with
length=1and it's safe to treat a string as a sequence of chars.I think it can be easily checked in the scanner.
cometkim commentedon Sep 8, 2024
When writing tokenizers in ReScript, It will still find something like this useful
cristianoc commentedon Sep 8, 2024
That's how one takes things apart.
I guess to put them back together, one will also use functions from String, correct?
cristianoc commentedon Sep 8, 2024
One observation is that the internal representation as integers might have to wait until after parsing, as pretty printing will restore the original.
Unless pretty printing itself will do some sort of normalization (when converting back from int).
github-actions commentedon Sep 4, 2025
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
fhammerschmidt commentedon Sep 4, 2025
Time flies...
hyperpolymath commentedon Apr 3, 2026
Hey — just wanted to drop some real-world signal on this RFC from a large-ish codebase migration.
I maintain IDApTIK, a ~542
.resfile browser game (PixiJS, reversible VM, multiplayer sync). We've been running a structured v12 → v13 migration test on a staging fork, and thechar/Unicode situation was directly relevant.What we hit: On Linux (Fedora, UTF-8 locale),
rescript.exewould crash on our codebase — we had to userescript-legacyas a workaround for the entire v12 cycle. We were specifically testing in our v13 migration plan (Phase 4) whether the parser rewrite to Rust (#8239) resolved this. I suspect it did — the Rust parser handles encoding differently from the OCaml one — but I haven't been able to confirm yet as v13 testing is still in progress.What I was working on: I had a root cause analysis and fix plan drafted for the UTF-8 crash, covering the encoding assumptions in the compiler binary. The parser rewrite likely made that moot for the crash specifically, but the underlying
chartype semantics issue this RFC addresses is still very much present.Supporting the RFC from practical experience:
char→ UTF-16 codepoint silent truncation is a real footgun. In a game with i18n support (we have A2ML-based localisation), you encounter non-BMP characters regularly. The current behaviour of silently discarding everything past the first codepoint is genuinely dangerous.Stdlib_Charremoval (remove Stdlib_Char module #7367) was a good step. We migrated offCharmodule usage without issues.48..57) would be very welcome — we have several places in our VM instruction decoder where we pattern match on character codes and currently have to use guard clauses.Basically: looks like the parser rewrite may have sorted our specific crash (great work on that!), but this RFC's core proposal — making
charan alias ofint, restricting literals to BMP, and enabling int range patterns — would still be a meaningful improvement. Happy to provide migration testing data from our codebase if that's useful for scoping the implementation.cknitt commentedon Oct 18, 2026
Here's where this stands now.
Already done since the RFC was opened:
Charmodule is gone.'👋🏽'(hand + skin-tone modifier) is now a syntax error instead of being truncated to'👋'.Pconst_char {source; semantic}), so the formatter prints literals back exactly as they were written.String.make(10, '𐀀')problem went away with the OCamlStringmodule.What's left, and my proposal:
Make
charan alias ofint. Chars are already plain integers at runtime, and the separate type mostly forcesObj.magicwhen you want to do arithmetic on code points. Existing code that type-checks keeps working. Literal syntax stays.Don't limit literals to the BMP. I'd keep the full Unicode range. Reading characters goes through
codePointAt, which returns full code points ('😀'→0x1F600). With a BMP limit,switch String.codePointAt(s, i) { | Some('😀') => ... }couldn't be written. The limit would only make sense if we modelled UTF-16 code units (charCodeAt), and we don't.Integer range patterns (
48 .. 57). Yes, but this needs real work. Today'a' .. 'z'is expanded into an or-pattern with one case per value, which doesn't scale to arbitrary int ranges. Doing this properly means intervals in the typed tree, interval-aware exhaustiveness checks, and switch compilation that emitsx >= a && x <= b. I'd track this as its own issue. Char ranges would then use the same mechanism.I suggest doing (1) as a small PR and opening a separate issue for (3). We can then close this RFC.
hyperpolymath commentedon Oct 18, 2026
Thanks Christoph, this clears things up for me. Keeping full code points makes sense; I’d been conflating that with the problem of literals silently losing extra characters in my earlier comment.
I’ve also seen the PRs since this reply. Having char literals work directly with ints, along with integer ranges, looks like it would cover what I was hoping for on the IDApTIK side.
Make `char` an alias of `int` (#8892)
cknitt commentedon Oct 19, 2026
This is now implemented; all parts landed on
master:charis an alias ofint(Makecharan alias ofint#8892). Char literals and ints mix freely, including in oneswitch, and the result ofString.codePointAt(option<int>) can be matched against char patterns directly:codePointAtreturns full code points.'\u{0}' .. '\u{10FFFF}'takes 0.02 s instead of about 16 s, and missing-case warnings show ranges instead of listing characters.| 200 .. 299 => ...works, and bounds may mix ints and chars.Along the way, #8896 fixed a compiler crash on an unreachable guarded or-pattern that turned up while testing.
Closing, since everything proposed here is done. Thanks everyone for the discussion!