Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Introducing the Bigint #6525

Closed
mununki opened this issue Dec 13, 2023 · 36 comments
Closed

RFC: Introducing the Bigint #6525

mununki opened this issue Dec 13, 2023 · 36 comments
Assignees

Comments

@mununki
Copy link
Member

mununki commented Dec 13, 2023

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
@zth
Copy link
Collaborator

zth commented Dec 13, 2023

Looking great! But I don't think the arithmetic operators can overlap with the regular int ones...? cc @cristianoc

@cometkim
Copy link
Member

may related with #6477 and https://forum.rescript-lang.org/t/new-operator-for-bigint-support/3922

@tsnobip
Copy link
Contributor

tsnobip commented Dec 13, 2023

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?

@zth
Copy link
Collaborator

zth commented Dec 13, 2023

Can't we just open the BigInt module for this?

How do you mean?

@mununki
Copy link
Member Author

mununki commented Dec 13, 2023

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.

@mununki
Copy link
Member Author

mununki commented Dec 13, 2023

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.

@tsnobip
Copy link
Contributor

tsnobip commented Dec 13, 2023

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.

@mununki
Copy link
Member Author

mununki commented Dec 13, 2023

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?

@CarlOlson
Copy link

CarlOlson commented Dec 14, 2023

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.

@mununki
Copy link
Member Author

mununki commented Dec 14, 2023

@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

@mununki
Copy link
Member Author

mununki commented Dec 14, 2023

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?

@tsnobip
Copy link
Contributor

tsnobip commented Dec 14, 2023

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.

@mununki
Copy link
Member Author

mununki commented Dec 14, 2023

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.

@cometkim
Copy link
Member

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#

@cometkim
Copy link
Member

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.

@tsnobip
Copy link
Contributor

tsnobip commented Dec 14, 2023

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)

@CarlOlson
Copy link

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...

@tsnobip
Copy link
Contributor

tsnobip commented Dec 14, 2023

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 +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...

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?

@cometkim
Copy link
Member

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 #6477 (comment). BigInt is usually used in isolated routines because it doesn't support polymorphic operations

@cometkim
Copy link
Member

a backlink to an old discussion #4677

@DZakh
Copy link
Contributor

DZakh commented Dec 14, 2023

I personally don't like addition of custom operators (+! or +n):

  1. It's already confusing to have +, +., ++ from the js user perspective
  2. BigInt is a rarely used feature. Mostly because it's not supported in older safari versions.

@cometkim
Copy link
Member

cometkim commented Dec 14, 2023

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...)

@mununki
Copy link
Member Author

mununki commented Dec 14, 2023

We do need the bigint for AOC 2024! (just kidding 😄)

@leostera
Copy link
Collaborator

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.

@DZakh
Copy link
Contributor

DZakh commented Dec 14, 2023

But you can already use BigInt with rescript-core

@cometkim
Copy link
Member

cometkim commented Dec 14, 2023

But you can already use BigInt with rescript-core

  • BigInt("100000") vs 100000n is different. n literal is more compact, statically checked, and optimized. (See Add BigInt primitive number type #4677 (comment))
  • 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.

@mununki
Copy link
Member Author

mununki commented Dec 14, 2023

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.

@DZakh
Copy link
Contributor

DZakh commented Dec 15, 2023

I have nothing against of having it as primitive type

@zth
Copy link
Collaborator

zth commented Dec 15, 2023

We'll need to have represented as a primitive in the compiler anyway for it to work with untagged variants etc.

@mununki
Copy link
Member Author

mununki commented Dec 17, 2023

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.

@zth
Copy link
Collaborator

zth commented Dec 17, 2023

I think we could combine having it a a primitive type in the compiler and having you open BigInt if you want to use the regular arithmetic operators...? Seems like a reasonable tradeoff to me.

@mununki
Copy link
Member Author

mununki commented Dec 18, 2023

I think we could combine having it a a primitive type in the compiler and having you open BigInt if you want to use the regular arithmetic operators...? Seems like a reasonable tradeoff to me.

It's nice to be able to do it in stages and not be radical. When you say open Bigint, do you mean that the arithmetic operators are the same as int?

@mununki
Copy link
Member Author

mununki commented Dec 18, 2023

If so, we still need to define the operator for bigint in case of not opening Bigint as default.

@mirko-plowtech
Copy link

mirko-plowtech commented Feb 28, 2024

why not just use BigInt.add, BigInt.minus, BigInt.equal and so on? ...I mean, I know that for equations is not the best UX, but I'm sure is better than no having support for those operations yet.

@CarlOlson
Copy link

why not just use BigInt.add, BigInt.minus, BigInt.equal and so on? ...I mean, I know that for equations is not the best UX, but I'm sure is better than no having support for those operations yet.

FWIW you can already do that. Many people have been using their own BigInt libraries for years. The easiest way is to bind those functions to raw JS.

@mununki mununki self-assigned this Mar 26, 2024
@mununki
Copy link
Member Author

mununki commented Mar 27, 2024

Resolved #6670

@mununki mununki closed this as completed Mar 27, 2024
cometkim added a commit that referenced this issue Nov 6, 2024
Introduce unified operators, the ad-hoc specialization for primitive operators.

For example adding two values, we have `+` for ints, `+.` for floats, and `++` for strings.
That is because we don't allow implicit conversion or overloading for operations.

It is a fundamental property of the ReScript language, but it is far from the best DX we can think of,
and it became a problem when new primitives like bigint were introduced.

See discussion: #6525

Unified ops mitigate the problem by adding ad-hoc translation rules on applications of the core built-in operators
which have a form of binary ('a -> 'a -> 'a) or unary ('a -> 'a)

Translation rules should be applied in its application, in both type-level and IR(lambda)-level.

The rules:

1. If the lhs type is a primitive type, unify the rhs and the result type to the lhs type.
2. If the lhs type is not a primitive type but the rhs type is, unify lhs and the result type to the rhs type.
3. If both lhs type and rhs type is not a primitive type, unify the whole types to the int. 

Since these are simple ad-hoc translations for primitive applications,
we cannot use the result type defined in other contexts.

So falling back to int type is the simplest behavior that ensures backward compatibility.

You can find related definitions on `ml/unified_ops.ml` file.

The actual implementation of translation is colocated into other modules.
- Type-level : `ml/typecore.ml`
- IR-level : `ml/translcore.ml`

You can find it with the function name `translate_unified_ops`

Resolved #6477
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

No branches or pull requests

8 participants