rescript-compiler
rescript-compiler copied to clipboard
RFC: Introducing the Bigint
Propose the Bigint in JS as a built-in type. mdn: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/BigInt
Background
BigInt is a new primitive that provides a way to represent whole numbers larger than 2^53, which is the largest number Javascript can reliably represent with the Number primitive. It is not introduced yet to ReScript yet.
Proposal
Literal
nativeint is actually locked in ReScript. I'd like to suggest using the 'n' suffix for the bigint literal.
1n
Operators
Basically mostly same to the integer operators.
// arithmetic
1n + 1n
1n - 1n
1n * 1n
1n / 1n
2n ** 54n
// neg
-1n
// comparisons
2n > 1n
1n < 2n
1n >= 1n
1n <= 1n
1n == 1n
1n === 1n
1n != 1n
1n !== 1n
Looking great! But I don't think the arithmetic operators can overlap with the regular int ones...? cc @cristianoc
may related with #6477 and https://forum.rescript-lang.org/t/new-operator-for-bigint-support/3922
Looking great! But I don't think the arithmetic operators can overlap with the regular int ones...? cc @cristianoc
Can't we just open the BigInt module for this?
Can't we just open the BigInt module for this?
How do you mean?
Looking great! But I don't think the arithmetic operators can overlap with the regular int ones...? cc @cristianoc
Yes, we can define new operator for the bigint, we can talk about in this thread. At the same time, I'm going to try to implement to make it using same operator with int, wonder how it looks.
Can't we just open the BigInt module for this?
If I understand correctly, @tsnobip meant that Belt.Result.t<'a,b'> before it is moved to the predef.
I just meant that the regular + is defined for big ints in RescriptCore.BigInt module for example, so when you open BigInt, you can use the regular + for big ints, and of course you won't be able to use it for regular ints anymore.
We can also define a special syntax for the big int operators, like +! for example.
Similar to @cometkim suggestion, I'm wondering why the comparison operator only checks if the two arguments are the same type, and the arithmetic operator should be split between integer and float. Maybe it's for compiler efficiency, but why not just check if the two arguments are the same type like the comparison operator?
I'm wondering why the comparison operator only checks if the two arguments are the same type, and the arithmetic operator should be split between integer and float.
Ocaml assigns types based on function usage. You need types assigned for certain optimizations. You couldn't have a compilation unit that exports a polymorphic function and expect it to work on BigInts, Floats, and Ints.
For example, compare the output of the two functions:
module Int = {
external \"+": ('a, 'a) => 'a = "%addint"
external \"-": ('a, 'a) => 'a = "%subint"
external \"*": ('a, 'a) => 'a = "%mulint"
external \"/": ('a, 'a) => 'a = "%divint"
let add1 = (x) => x + (x / x)
}
module Float = {
external \"+": ('a, 'a) => 'a = "%addfloat"
external \"-": ('a, 'a) => 'a = "%subfloat"
external \"*": ('a, 'a) => 'a = "%mulfloat"
external \"/": ('a, 'a) => 'a = "%divfloat"
let add1 = (x) => x + (x / x)
}
If you look at the source of polymorphic comparisons, it is much more complex. If you need performance, you should avoid using it.
@CarlOlson Yes, indeed. The typechecking logics are quite complex than I expect 😄
Actually, I changed the arithmetic operations to polymorphic for int and bigint for test. I haven't benchmarked it yet, but I'm sure there will be a downgrade in compilation performance, and I'll have to modify the type checker to not accept any types other than int, float, and bigint.
let d = 1 + 1
let e = 1n + 1n
let f = 1.0 + 1.0
let g = "a" + "a" // should be type error
let h =
{
() => 1
}() + 1
I'm thinking it's a choice between adding another operator for bigint and changing the operators to a polymorphic type, what do you guys think?
You could actually even keep it for strings, so it's like JS.
But I wouldn't trade any performance for this! If it has an impact, then adding operators for big ints would feel natural.
You could actually even keep it for strings, so it's like JS.
I think the JS output maybe look okay, but looks weird in syntax layer. we have string concatenation ++ already.
It's not that simple. It's what the rest of the world calls type classes (or extensions), and OCaml has been evaluating it for a decade (modular implicit).
What I proposed in #6477 is a much simpler solution and is how already done in F#
I'm wondering why the comparison operator only checks if the two arguments are the same type, and the arithmetic operator should be split between integer and float.
This is absolutely necessary. The polymorphic + in JS actually differentiates int32 inside the engine. Thanks to ReScript's %addint, it is very friendly to JIT optimization. On the other hand, BigInt's + is not polymorphic. It will check the type early and throw a TypeError.
Let's stick to a new set of custom operators then?
For the syntax itself, I have 0 inspiration, I'd say +! or +n (but this one feels a bit weird though consistent)
For the syntax itself, I have 0 inspiration, I'd say
+!or+n(but this one feels a bit weird though consistent)
Is there inspiration for the +! idea? I feel like +n is a bit more obvious for users coming from JS. It also would make future operators obvious, like +b if someone created a "bignum" library. Or +c for complex numbers, etc...
Is there inspiration for the
+!idea?
Nothing precise, just the fact that we need some character that is not already a math operator (+* or +- would obviously be weird), and in my head the bang ! can imply something big, like big ints :)
I feel like
+nis a bit more obvious for users coming from JS. It also would make future operators obvious, like+bif someone created a "bignum" library. Or+cfor complex numbers, etc...
The issue is that it'd be a first combination of a alphabet letter with a mathematical symbol and it would break the fact that symbols don't need to be surrounded with spaces:
let m = 2
let n = 1
let res = m+n
// this is equivalent to
// let res = m + n
// what would happen now?
Or, we can introduce only the obvious parts, such as type, literals, and untagged variants, and leave operators only in std modules as suggested in https://github.com/rescript-lang/rescript-compiler/issues/6477#issuecomment-1820645145. BigInt is usually used in isolated routines because it doesn't support polymorphic operations
a backlink to an old discussion #4677
I personally don't like addition of custom operators (+! or +n):
- It's already confusing to have
+,+.,++from the js user perspective - BigInt is a rarely used feature. Mostly because it's not supported in older safari versions.
BigInt is a rarely used feature. Mostly because it's not supported in older safari versions.
In my product, all DB IDs are treated as bigint (bigserial). protobuf-js also relies on bigint (fallback to long.js) and I actually stopped using ReScript when developing a similar codec because of missing bigint support.
bigint is much more common and has already achieved over 96% compatibility according to caniuse data.
Even the recent version of XCode has started to end support for iOS 14 and lower. (There is no emulator...)
We do need the bigint for AOC 2024! (just kidding 😄)
I think explicitly opening the BigInt module to get convenient operators isn't too much to ask. If you're working on a BigInt heavy codebase you can open it by default, and otherwise you can open it wherever you need to.
I'd rather do that than figure out which flavor-of-the-day operator I need to use for this type.
But you can already use BigInt with rescript-core
But you can already use BigInt with rescript-core
BigInt("100000")vs100000nis different. n literal is more compact, statically checked, and optimized. (See https://github.com/rescript-lang/rescript-compiler/issues/4677#issuecomment-1002980370)- BigInt is a "primitive" in JS. Support for primitives is the basis for our interoperability claims.
- It should be handled explicitly when we work with its closely related parts, such as untagged variants.
IMHO, there are a lot of advantages to having it as a primitive type, and if we're stuck with the discussion of opening up the Bigint module instead of having it as a primitive type, it would be nice to have reasons why it's worse to have it as a primitive type.
I have nothing against of having it as primitive type
We'll need to have represented as a primitive in the compiler anyway for it to work with untagged variants etc.
If we decide to go with primitive types, I think the debate is split between adding new operators and making the arithmetic operators polymorphic. Adding new operators is simpler than the latter. I'll try to prepare a PR for a poc to make the arithmetic operators polymorphic soon, and it would be nice to see the implementation and performance and continue the discussion.