-
Notifications
You must be signed in to change notification settings - Fork 455
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
Introducing the bigint #6670
Introducing the bigint #6670
Conversation
jscomp/others/js_json.res
Outdated
@@ -30,6 +30,7 @@ type rec t = | |||
| @as(null) Null | |||
| String(string) | |||
| Number(float) | |||
| Bigint(bigint) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BigInt doesn't belong to JSON spec
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, correct. I'll remove it, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed c8b78d2
Can be done in a follow up PR of course, but adding support for it in untagged variants would be nice. |
Oh, this PR already added support the untagged variant with Bigint. |
@mununki haha I'm sorry, you're right... Well done! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work! 🎉 This will be great to support. A few smaller questions from me. @cristianoc should have a look at this as well.
I'm not sold on the comma. Comma has such a specific meaning in the language that the code looks broken to me when there's a comma. I don't have a better suggestion yet though, but maybe we could figure one out together.
jscomp/ml/ast_payload.ml
Outdated
({pexp_desc = Pexp_constant (Pconst_integer (name, Some n)); _}, _); | ||
_; | ||
}; | ||
] when n = 'n' -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit, but can't this when clause just be directly inside of the pattern?
jscomp/ml/ast_untagged_variants.ml
Outdated
@@ -375,7 +393,7 @@ module DynamicChecks = struct | |||
in | |||
let literals_overlaps_with_number () = | |||
Ext_list.exists literal_cases (function | |||
| Int _ | Float _ -> true | |||
| Int _ | Float _ | Bigint _ -> true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bigint doesn't overlap with number, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! a9e4c16
jscomp/others/js_bigint.res
Outdated
@@ -1,3 +1,11 @@ | |||
/*** JavaScript BigInt API */ | |||
|
|||
type t | |||
type t = bigint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering whether we should just remove this one and refer to bigint
directly instead. It's breaking so we could wait for a bit before doing that and just deprecate t
meanwhile, but we should definitely refer to bigint
instead of t
in the code below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! cf62db8
jscomp/others/js_bigint.res
Outdated
external \"~-": t => t = "%negbigint" | ||
external \"~+": t => t = "%identity" | ||
external \"+": (t, t) => t = "%addbigint" | ||
external \"-": (t, t) => t = "%subbigint" | ||
external \"*": (t, t) => t = "%mulbigint" | ||
external \"/": (t, t) => t = "%divbigint" | ||
external mod: (t, t) => t = "%modbigint" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As said above, I think t
should be bigint
instead here.
Having yet another syntax for one more numeric type seems to make an existing problem worse. The existing problem is having to use + and +. for ints and floats. I'd rather go the direction of making x+y resolve to int or float or bigint at type checking time. |
While we figure something like that out, can we get away with the following perhaps? // Using opens, described in this PR
open! Js.Bigint
let a = 1n + 1n
// Or, functions on bigint...?
let a = Js.Bigint.add(1n, 1n) |
This would be great, but how would it work with type inference? What type would the function let f = (x, y) => x + y have? |
Without type annotations, the default numeric type. |
suggested on #6477 actually we are doing same for comparison operators. |
I agree adding another number type to make a problem worse. I didn't think about adding the generic number type yet. I think it is very good direction to solve the problem having two different operators for int and float. I grab a chance to look into how int and float type value are handled during compilation while working on this PR. Actually, I couldn't find many reasons to separate two different type(int, float) for emitting the JS output except optimization of int32 for int type value. How about defining the |
I suspect a number of users rely on the semantic difference between int and float. Eg with float it's easy to get 0.999999 while 1 is expected so simple equality does not work. That's why I was suggesting a simple change at the level of type checker, which does not break existing code. |
If I understand correctly, Do you mean letting the type checker infer types for arithmetic operations like it does for comparisons? // Int operations
external \"+": (int, int) => int = "%addint"
// Float operations
external \"+.": (float, float) => float = "%addfloat"
// to
// Number operations
external \"+": ('a, 'a) => 'a = "%addnumber" |
Yes a bit like for comparison. |
To summarize, if we'll discuss the issue of unifying the int and float operators separately, and if everyone agrees that // exponentiation
let x = 1n ** 1n
// comparison optimization
let y = 1n == 1n
// compiled to var y = true; |
lib/es6/caml.js
Outdated
return 0; | ||
} else if (x < y) { | ||
return -1; | ||
} else if (x > y || x === x) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Weird or clause
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh good catch, I didn't notice because I was using the float_compare code as is. Why does float_compare have this branching as opposed to int_compare? Very weird to me too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think those conditions are for below test cases;
// float_test.res
eq(__LOC__, float_compare(Js.Float._NaN, Js.Float._NaN), 0)
eq(__LOC__, generic_compare(Js.Float._NaN, Js.Float._NaN), 0)
eq(__LOC__, float_compare(Js.Float._NaN, neg_infinity), -1)
eq(__LOC__, generic_compare(Js.Float._NaN, neg_infinity), -1)
eq(__LOC__, float_compare(neg_infinity, Js.Float._NaN), 1)
eq(__LOC__, generic_compare(neg_infinity, Js.Float._NaN), 1)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know why float_compare was implemented this way at the time, but my best guess is that it was to capitalize on the fact that comparing NaNs is false.
-Infinity === -Infinity // true
NaN === NaN // false
However, the bigint doesn't have a binding to Infinity or Nan for now. I'm going to fix bigint_compare
as int_compare
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed my mind. I've added interop for NaN
and methods to Js.Bigint, and also added the bigint_test.res
test. Can you take a look? c972a13
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is the compare function. Those clauses are for the comparing with NaN.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got your point. NaN can not be produced with BigInt operation. NaN is produced only below 5 cases:
There are five different types of operations that return NaN:
- Failed number conversion (e.g. explicit ones like parseInt("blabla"), Number(undefined), or implicit ones like Math.abs(undefined))
- Math operation where the result is not a real number (e.g. Math.sqrt(-1))
- Indeterminate form (e.g. 0 * Infinity, 1 ** Infinity, Infinity / Infinity, Infinity - Infinity)
- A method or expression whose operand is or gets coerced to NaN (e.g. 7 ** NaN, 7 * "blabla") — this means NaN is contagious
- Other cases where an invalid value is to be represented as a number (e.g. an invalid Date new Date("blabla").getTime(), "".charCodeAt(1))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So wouldn't it be better to get rid of NaNs in Js_bigint as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. NaN is obviously number type. Date, parseInt, Math are all about Number, not BigInt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I'm working on the comparison lambda, lam, I think I found there are some codes better to be cleaned up. E.g. the physical comparison ( |
I've finished adding the comparison operation optimization and exponetiation operator |
jscomp/others/js_bigint.res
Outdated
therefore necessary to test for `_NaN`. Return `true` if the given value is | ||
`_NaN`, `false` otherwise. See [`isNaN`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN) on MDN. | ||
*/ | ||
external isNaN: bigint => bool = "isNaN" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is necessary in bigint module. Unlike number, bigint operations (except comparison) cannot be mixed with other types and never produce NaN.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes sense to me.
jscomp/others/js_bigint.res
Outdated
Js.Bigint.isFinite(1234.) | ||
``` | ||
*/ | ||
external isFinite: bigint => bool = "isFinite" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here. Js BigInt never produces Infinity
lib/es6/caml.js
Outdated
return 0; | ||
} else if (x < y) { | ||
return -1; | ||
} else if (x > y || x === x) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NaN is a concept that belongs to Number (typeof NaN === number
), not BigInt. BigInt operations never produce NaN AFAIK
jscomp/ml/translcore.ml
Outdated
("%negbigint", Pnegbigint); | ||
("%addbigint", Paddbigint); | ||
("%subbigint", Psubbigint); | ||
("%mulbigint", Pmulbigint); | ||
("%divbigint", Pdivbigint Safe); | ||
("%powbigint", Ppowbigint); | ||
("%modbigint", Pmodbigint Safe); | ||
("%eqbigint", Pbigintcomp Ceq); | ||
("%noteqbigint", Pbigintcomp Cneq); | ||
("%ltbigint", Pbigintcomp Clt); | ||
("%lebigint", Pbigintcomp Cle); | ||
("%gtbigint", Pbigintcomp Cgt); | ||
("%gebigint", Pbigintcomp Cge); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems %lslbigint
(<<
), %asrbigint
(>>
), %andbigint
(&
), %orbigint
(|
), %xorbigint
(^
) are missing? (not sure bitwise-not(~
) op) Logical operators are the main interface of BigInt.
Note that >>>
, Unsigned right shift doesn't accept BigInt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initially, I wasn't going to include the bitwise operation implementation in this PR because I was concerned that it would make the PR too large, but now that I think about it, it doesn't make much difference, so I'll add it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added 9060aa0
Sorry for rebasing during review, there was a conflict in the change log file. |
let bigint_comp (cmp : Lam_compat.comparison) ?comment (e0: t) (e1: t) = | ||
bin ?comment (Lam_compile_util.jsop_of_comp cmp) e0 e1 | ||
|
||
let bigint_div ~checked ?comment (e0: t) (e1: t) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like the option to disable checking divide by zero is really only hidden under some ancient internal-only configuration
at some point, it can be removed, perhaps in a later PR
jscomp/core/lam_analysis.ml
Outdated
@@ -27,6 +27,7 @@ let not_zero_constant (x : Lam_constant.t) = | |||
match x with | |||
| Const_int { i } -> i <> 0l | |||
| Const_int64 i -> i <> 0L | |||
| Const_bigint i -> i <> "0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this normalised, or are -0
and 00
also possible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
00
is normalized, but -0
. I'll add another clause i <> "-0"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about instead using a representation for Conts_bigint
with a boolean (sign), and normalised string. So there's no confusion and it's less likely to introduce bugs in future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, very good. I'll refactor accordingly.
jscomp/others/js_bigint.res
Outdated
Js.Bigint.fromString("") | ||
|
||
/* returns 17n */ | ||
Js.Bigint.fromString("0x11") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is not supported as literal, but can be constructed in this way via parsing right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, users can use this way for unsupported literal such as 0x11n
jscomp/others/js_bigint.res
Outdated
@val | ||
/** | ||
Parses the given `string` into a `bigint` using JavaScript semantics. Return the | ||
number as a `bigint` if successfully parsed, `null`, `undefined`, `_NaN` otherwise. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure what null
, undefined
, _NaN
refers to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is invalid comment, I'll remove this part.
jscomp/others/js_bigint.res
Outdated
|
||
```rescript | ||
/* returns 123n */ | ||
Js.Bigint.fromString("123") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this called fromStringExn
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, you're correct.
Js.Bigint.fromString("0o11") | ||
``` | ||
*/ | ||
external fromStringExn: string => bigint = "BigInt" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the exception? It would be good to have an example to see how to catch it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JS BigInt constructor throws error such as Uncaught SyntaxError
in case of passing a non-numeric value. I'll add an example .
jscomp/ml/bigint_utils.ml
Outdated
| _ -> | ||
(* If both numbers are either negative or positive, compare their lengths. *) | ||
let len0, len1 = (String.length s0, String.length s1) in | ||
if len0 = len1 then String.compare s0 s1 (* If lengths are equal, compare the strings directly. *) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems wrong for negative numbers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I figure it out this case is wrong assert((compare "-10" "-12" = 1))
I'll fix it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jscomp/ml/bigint_utils.ml
Outdated
000n -> 0n | ||
001n -> 1n | ||
01_000_000n -> 1000000n | ||
0x1 -> 0x1n |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is now a syntax error too right?
jscomp/ml/bigint_utils.ml
Outdated
001n -> 1n | ||
01_000_000n -> 1000000n | ||
0x1 -> 0x1n | ||
0x001n -> 0x001n |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and this
@@ -300,6 +300,9 @@ let inline_int_primitive (i : int32) : string list = | |||
let inline_int64_primitive (i : int64) : string list = | |||
[""; to_string (Ffi_inline_const (Const_int64 i))] | |||
|
|||
let inline_bigint_primitive (i : string) : string list = | |||
[""; to_string (Ffi_inline_const (Const_bigint i))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not check that the bigint is valid, or remove leading zero's.
Don't know if this is a problem, just observing the fact.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One possibility is to do the check is_numeric
at parsing time, instead of type checking time. And do the normalisation (remove zeros) whenever a Const_bigint
is constructed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good. I'm going to try to refactor Const_bigint bool * string
in AST type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've changed the model of bigint in asttypes.ml
, lam_constant.ml
and js_op.ml
What do you think?
jscomp/core/js_op.ml
Outdated
type number = | ||
| Float of float_lit | ||
| Int of { i : int32; c : int option } | ||
| Uint of int32 | ||
| Bigint of bigint_lit | ||
| Bigint of bool * string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small style suggestion keep bigint_lit
as { positive: bool; value: string }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good, it seems not going to be unboxed, still okay?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The difference is going to be tiny in practice.
jscomp/core/js_exp_make.ml
Outdated
| Bigint i -> bigint i | ||
| Bigint i -> | ||
let open Bigint_utils in | ||
let sign, i = i |> remove_leading_sign in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style/naming: perhaps:
let {positive, value} = parse_bigint i
where parse_bigint
also removes the leading zeros, so there's no way one can forget to do that
jscomp/core/lam.ml
Outdated
@@ -506,7 +506,7 @@ let prim ~primitive:(prim : Lam_primitive.t) ~args loc : t = | |||
(* FIXME: could raise? *) | |||
Lift.bool | |||
(Lam_compat.cmp_float cmp (float_of_string a) (float_of_string b)) | |||
| Pbigintcomp cmp, Const_bigint a, Const_bigint b -> default () | |||
| Pbigintcomp cmp, Const_bigint (_, a), Const_bigint (_, b) -> default () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: here a
and b
are never used, I thought the compiler would give a warning
jscomp/ml/bigint_utils.ml
Outdated
if len = 0 then (false, str) | ||
else | ||
if is_neg str || is_pos str then (not (is_neg str), String.sub str 1 (len -1)) | ||
else (not (is_neg str), str) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: in this branch is_neg
is impossible, so just (true, str)
jscomp/ml/parmatch.ml
Outdated
(function Tpat_constant(Const_bigint i) -> String.length i | _ -> assert false) | ||
(function i -> Tpat_constant(Const_bigint (String.make i '*'))) | ||
(function Tpat_constant(Const_bigint (sign, i)) -> String.length (Bigint_utils.to_string sign i) | _ -> assert false) | ||
(function i -> Tpat_constant(Const_bigint (true, (String.make i '*')))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure what (String.make i '*')
is doing here?
I would have guessed string_of_int i
instead.
Not sure how to trigger this code path, perhaps with some example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure. I guess this function is used in pattern matching for gadt partially. But logically I think we can change (String.make i '*')
to string_of_int i
here for less space complexity.
jscomp/syntax/src/res_core.ml
Outdated
@@ -812,6 +812,12 @@ let parseConstant p = | |||
let constant = | |||
match p.Parser.token with | |||
| Int {i; suffix} -> | |||
(* Only decimal literal is allowed for bigint *) | |||
if suffix = Some 'n' && not (Bigint_utils.is_numeric i) then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: perhaps is_numeric
could be called is_valid
?
jscomp/syntax/src/res_core.ml
Outdated
if suffix = Some 'n' && not (Bigint_utils.is_numeric i) then | ||
Parser.err p | ||
(Diagnostics.message | ||
"Invalid decimal literal. Only decimal literal is allowed for \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps "Invalid bigint literal. Only decimal...".
Fixed as reviewed. d82b4bc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to go!
Anything missing?
Nothing left. |
Fantastic @mununki , well done! 👏 |
Discussions: #6525
This PR is to introduce the JS bigint as a primitive type. With the addition of the bigint type, this PR implements support for basic arithmetic operators and unary negation operators as a first step. The comparison operator and exponential operator is being added to the initial implementation of this PR for review.
I tried to make all arithmetic operations polymorphic, but the downside of stripping out the optimization logic for existing int and float was that we explicitly added operators.
The main changes are
*n
literal of nativeint for bigint.Added arithmetic operators. (The comma can be changed to something better on the discussion. I initially considered+n
, but saw the increased complexity in parsing and implemented it as a comma).VeryLargeNumber(bigint)
Js.Bigint
module to override the integer operator depending on user needs.