boa_engine/builtins/bigint/mod.rs
1//! Boa's implementation of ECMAScript's global `BigInt` object.
2//!
3//! `BigInt` is a built-in object that provides a way to represent whole numbers larger
4//! than the largest number JavaScript can reliably represent with the Number primitive
5//! and represented by the `Number.MAX_SAFE_INTEGER` constant.
6//! `BigInt` can be used for arbitrarily large integers.
7//!
8//! More information:
9//! - [ECMAScript reference][spec]
10//! - [MDN documentation][mdn]
11//!
12//! [spec]: https://tc39.es/ecma262/#sec-bigint-objects
13//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt
14
15use crate::{
16 Context, JsArgs, JsBigInt, JsExpect, JsResult, JsString, JsValue,
17 builtins::BuiltInObject,
18 context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
19 error::JsNativeError,
20 js_string,
21 object::JsObject,
22 property::Attribute,
23 realm::Realm,
24 string::StaticJsStrings,
25 symbol::JsSymbol,
26 value::{IntegerOrInfinity, PreferredType},
27};
28use num_bigint::ToBigInt;
29
30use super::{BuiltInBuilder, BuiltInConstructor, IntrinsicObject};
31
32#[cfg(test)]
33mod tests;
34
35/// `BigInt` implementation.
36#[derive(Debug, Clone, Copy)]
37pub struct BigInt;
38
39impl IntrinsicObject for BigInt {
40 fn init(realm: &Realm) {
41 BuiltInBuilder::from_standard_constructor::<Self>(realm)
42 .method(Self::to_string, js_string!("toString"), 0)
43 .method(Self::to_locale_string, js_string!("toLocaleString"), 0)
44 .method(Self::value_of, js_string!("valueOf"), 0)
45 .static_method(Self::as_int_n, js_string!("asIntN"), 2)
46 .static_method(Self::as_uint_n, js_string!("asUintN"), 2)
47 .property(
48 JsSymbol::to_string_tag(),
49 Self::NAME,
50 Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
51 )
52 .build();
53 }
54
55 fn get(intrinsics: &Intrinsics) -> JsObject {
56 Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
57 }
58}
59
60impl BuiltInObject for BigInt {
61 const NAME: JsString = StaticJsStrings::BIG_INT;
62}
63
64impl BuiltInConstructor for BigInt {
65 const CONSTRUCTOR_ARGUMENTS: usize = 1;
66 const PROTOTYPE_STORAGE_SLOTS: usize = 4;
67 const CONSTRUCTOR_STORAGE_SLOTS: usize = 2;
68
69 const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
70 StandardConstructors::bigint;
71
72 /// `BigInt()`
73 ///
74 /// The `BigInt()` constructor is used to create `BigInt` objects.
75 ///
76 /// More information:
77 /// - [ECMAScript reference][spec]
78 /// - [MDN documentation][mdn]
79 ///
80 /// [spec]: https://tc39.es/ecma262/#sec-bigint-objects
81 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/BigInt
82 fn constructor(
83 new_target: &JsValue,
84 args: &[JsValue],
85 context: &mut Context,
86 ) -> JsResult<JsValue> {
87 // 1. If NewTarget is not undefined, throw a TypeError exception.
88 if !new_target.is_undefined() {
89 return Err(JsNativeError::typ()
90 .with_message("BigInt is not a constructor")
91 .into());
92 }
93
94 let value = args.get_or_undefined(0);
95
96 // 2. Let prim be ? ToPrimitive(value, number).
97 let prim = value.to_primitive(context, PreferredType::Number)?;
98
99 // 3. If Type(prim) is Number, return ? NumberToBigInt(prim).
100 if let Some(number) = prim.as_number() {
101 return Self::number_to_bigint(number);
102 }
103
104 // 4. Otherwise, return ? ToBigInt(prim).
105 Ok(prim.to_bigint(context)?.into())
106 }
107}
108
109impl BigInt {
110 /// `NumberToBigInt ( number )`
111 ///
112 /// More information:
113 /// - [ECMAScript reference][spec]
114 ///
115 /// [spec]: https://tc39.es/ecma262/#sec-numbertobigint
116 fn number_to_bigint(number: f64) -> JsResult<JsValue> {
117 // 1. If IsIntegralNumber(number) is false, throw a RangeError exception.
118 if number.is_nan() || number.is_infinite() || number.fract() != 0.0 {
119 return Err(JsNativeError::range()
120 .with_message(format!("cannot convert {number} to a BigInt"))
121 .into());
122 }
123
124 // 2. Return the BigInt value that represents ℝ(number).
125 Ok(JsBigInt::from(
126 number
127 .to_bigint()
128 .js_expect("This conversion must be safe")?,
129 )
130 .into())
131 }
132
133 /// The abstract operation `thisBigIntValue` takes argument value.
134 ///
135 /// The phrase “this `BigInt` value” within the specification of a method refers to the
136 /// result returned by calling the abstract operation `thisBigIntValue` with the `this` value
137 /// of the method invocation passed as the argument.
138 ///
139 /// More information:
140 /// - [ECMAScript reference][spec]
141 ///
142 /// [spec]: https://tc39.es/ecma262/#sec-thisbigintvalue
143 fn this_bigint_value(value: &JsValue) -> JsResult<JsBigInt> {
144 value
145 // 1. If Type(value) is BigInt, return value.
146 .as_bigint()
147 // 2. If Type(value) is Object and value has a [[BigIntData]] internal slot, then
148 // a. Assert: Type(value.[[BigIntData]]) is BigInt.
149 // b. Return value.[[BigIntData]].
150 .or_else(|| {
151 value
152 .as_object()
153 .and_then(|obj| obj.downcast_ref::<JsBigInt>().as_deref().cloned())
154 })
155 // 3. Throw a TypeError exception.
156 .ok_or_else(|| {
157 JsNativeError::typ()
158 .with_message("'this' is not a BigInt")
159 .into()
160 })
161 }
162
163 /// `BigInt.prototype.toString( [radix] )`
164 ///
165 /// The `toString()` method returns a string representing the specified `BigInt` object.
166 ///
167 /// More information:
168 /// - [ECMAScript reference][spec]
169 /// - [MDN documentation][mdn]
170 ///
171 /// [spec]: https://tc39.es/ecma262/#sec-bigint.prototype.tostring
172 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/toString
173 #[allow(clippy::wrong_self_convention)]
174 pub(crate) fn to_string(
175 this: &JsValue,
176 args: &[JsValue],
177 context: &mut Context,
178 ) -> JsResult<JsValue> {
179 // 1. Let x be ? thisBigIntValue(this value).
180 let x = Self::this_bigint_value(this)?;
181
182 let radix = args.get_or_undefined(0);
183
184 // 2. If radix is undefined, let radixMV be 10.
185 let radix_mv = if radix.is_undefined() {
186 // 5. If radixMV = 10, return ! ToString(x).
187 // Note: early return optimization.
188 return Ok(js_string!(x.to_string()).into());
189 // 3. Else, let radixMV be ? ToIntegerOrInfinity(radix).
190 } else {
191 radix.to_integer_or_infinity(context)?
192 };
193
194 // 4. If radixMV < 2 or radixMV > 36, throw a RangeError exception.
195 let radix_mv = match radix_mv {
196 IntegerOrInfinity::Integer(i) if (2..=36).contains(&i) => i,
197 _ => {
198 return Err(JsNativeError::range()
199 .with_message("radix must be an integer at least 2 and no greater than 36")
200 .into());
201 }
202 };
203
204 // 5. If radixMV = 10, return ! ToString(x).
205 if radix_mv == 10 {
206 return Ok(js_string!(x.to_string()).into());
207 }
208
209 // 1. Let x be ? thisBigIntValue(this value).
210 // 6. Return the String representation of this Number value using the radix specified by radixMV.
211 // Letters a-z are used for digits with values 10 through 35.
212 // The precise algorithm is implementation-defined, however the algorithm should be a generalization of that specified in 6.1.6.2.23.
213 Ok(JsValue::new(js_string!(x.to_string_radix(radix_mv as u32))))
214 }
215
216 #[allow(
217 unused_variables,
218 reason = "`args` is used if the `intl` feature is enabled"
219 )]
220 fn to_locale_string(
221 this: &JsValue,
222 args: &[JsValue],
223 context: &mut Context,
224 ) -> JsResult<JsValue> {
225 #[cfg(feature = "intl")]
226 {
227 use crate::builtins::intl::NumberFormat;
228 use fixed_decimal::Decimal;
229 use writeable::Writeable;
230
231 // 1. Let x be ? ThisBigIntValue(this value).
232 let x = Self::this_bigint_value(this)?;
233 let locales = args.get_or_undefined(0).clone();
234 let options = args.get_or_undefined(1).clone();
235
236 // 2. Let numberFormat be ? Construct(%Intl.NumberFormat%, « locales, options »).
237 let number_format = NumberFormat::new(&locales, &options, context)?;
238 let x = &mut Decimal::try_from_str(&x.to_string())
239 .map_err(|err| JsNativeError::range().with_message(err.to_string()))?;
240
241 // 3. Return FormatNumeric(numberFormat, ℝ(x)).
242 Ok(js_string!(number_format.format(x).write_to_string()).into())
243 }
244
245 #[cfg(not(feature = "intl"))]
246 {
247 // Arguments are reserved, so we cannot pass them to the `toString` method.
248 Self::to_string(this, &[], context)
249 }
250 }
251
252 /// `BigInt.prototype.valueOf()`
253 ///
254 /// The `valueOf()` method returns the wrapped primitive value of a Number object.
255 ///
256 /// More information:
257 /// - [ECMAScript reference][spec]
258 /// - [MDN documentation][mdn]
259 ///
260 /// [spec]: https://tc39.es/ecma262/#sec-bigint.prototype.valueof
261 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/valueOf
262 pub(crate) fn value_of(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
263 Ok(JsValue::new(Self::this_bigint_value(this)?))
264 }
265
266 /// `BigInt.asIntN()`
267 ///
268 /// The `BigInt.asIntN()` method wraps the value of a `BigInt` to a signed integer between `-2**(width - 1)` and `2**(width-1) - 1`.
269 ///
270 /// [spec]: https://tc39.es/ecma262/#sec-bigint.asintn
271 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/asIntN
272 #[allow(clippy::wrong_self_convention)]
273 pub(crate) fn as_int_n(
274 _: &JsValue,
275 args: &[JsValue],
276 context: &mut Context,
277 ) -> JsResult<JsValue> {
278 let (modulo, bits) = Self::calculate_as_uint_n(args, context)?;
279
280 if bits > 0
281 && modulo >= JsBigInt::pow(&JsBigInt::new(2), &JsBigInt::new(i64::from(bits) - 1))?
282 {
283 Ok(JsValue::new(JsBigInt::sub(
284 &modulo,
285 &JsBigInt::pow(&JsBigInt::new(2), &JsBigInt::new(i64::from(bits)))?,
286 )))
287 } else {
288 Ok(JsValue::new(modulo))
289 }
290 }
291
292 /// `BigInt.asUintN()`
293 ///
294 /// The `BigInt.asUintN()` method wraps the value of a `BigInt` to an unsigned integer between `0` and `2**(width) - 1`.
295 ///
296 /// [spec]: https://tc39.es/ecma262/#sec-bigint.asuintn
297 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/asUintN
298 #[allow(clippy::wrong_self_convention)]
299 pub(crate) fn as_uint_n(
300 _: &JsValue,
301 args: &[JsValue],
302 context: &mut Context,
303 ) -> JsResult<JsValue> {
304 let (modulo, _) = Self::calculate_as_uint_n(args, context)?;
305
306 Ok(JsValue::new(modulo))
307 }
308
309 /// Helper function to wrap the value of a `BigInt` to an unsigned integer.
310 ///
311 /// This function expects the same arguments as `as_uint_n` and wraps the value of a `BigInt`.
312 /// Additionally to the wrapped unsigned value it returns the converted `bits` argument, so it
313 /// can be reused from the `as_int_n` method.
314 fn calculate_as_uint_n(args: &[JsValue], context: &mut Context) -> JsResult<(JsBigInt, u32)> {
315 let bits_arg = args.get_or_undefined(0);
316 let bigint_arg = args.get_or_undefined(1);
317
318 let bits = bits_arg.to_index(context)?;
319 let bits = u32::try_from(bits).unwrap_or(u32::MAX);
320
321 let bigint = bigint_arg.to_bigint(context)?;
322
323 Ok((
324 JsBigInt::mod_floor(
325 &bigint,
326 &JsBigInt::pow(&JsBigInt::new(2), &JsBigInt::new(i64::from(bits)))?,
327 ),
328 bits,
329 ))
330 }
331}