Skip to main content

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}