Skip to main content

quickjs_oxide/engine/value/
bigint.rs

1//! `QuickJS`-compatible arbitrary-precision integer values.
2//!
3//! This module mirrors the two representations used by `QuickJS` 2026-06-04:
4//! values fitting in one signed host limb use an immediate `i64`, while larger
5//! values are reference-counted arbitrary-precision integers. Every operation
6//! compacts its result back to the immediate representation when possible.
7//!
8//! `QuickJS` stores heap BigInts as normalized, little-endian two's-complement
9//! limbs. `num_bigint::BigInt` is sign/magnitude internally, so the few places
10//! where the representation is observable (notably `BigInt.asUintN`) explicitly
11//! emulate the upstream signed-limb behavior.
12
13use super::primitive::is_ecmascript_whitespace;
14use std::borrow::Cow;
15use std::cmp::Ordering;
16use std::fmt;
17use std::rc::Rc;
18use std::str::FromStr;
19
20use num_bigint::{BigInt, BigUint, Sign};
21use num_traits::{FromPrimitive, One, ToPrimitive, Zero};
22
23/// `QuickJS` normally limits a BigInt allocation to
24/// `(1024 * 1024) / JS_LIMB_BITS` limbs.
25///
26/// On the 64-bit compatibility target this is 16,384 limbs, or 1,048,576
27/// signed representation bits. `js_bigint_extend` can append one sign limb
28/// after that guarded allocation, so selected add/sub/neg/shift results may
29/// temporarily occupy 16,385 limbs exactly as in the pinned release.
30pub const MAX_BIGINT_BITS: u64 = 1024 * 1024;
31
32/// The host limb width used by the `QuickJS` 64-bit value representation.
33pub const BIGINT_LIMB_BITS: u64 = 64;
34
35const MAX_BIGINT_LIMBS: u64 = MAX_BIGINT_BITS / BIGINT_LIMB_BITS;
36
37/// A failure produced by the BigInt value layer.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum BigIntError {
40    /// A textual value is not a valid BigInt.
41    InvalidSyntax,
42    /// A formatting or parsing radix is outside `2..=36`.
43    InvalidRadix(u32),
44    /// Division or remainder by zero was requested.
45    DivisionByZero,
46    /// Exponentiation was requested with a negative exponent.
47    NegativeExponent,
48    /// The result would exceed the upstream BigInt allocation limit.
49    BigIntTooLarge,
50    /// An operation tried to allocate an input-sized BigInt whose normalized
51    /// representation came from QuickJS's one-limb extension bypass.
52    AllocationTooLarge,
53    /// A left shift would exceed the upstream BigInt allocation limit.
54    ShiftTooLarge,
55}
56
57impl fmt::Display for BigIntError {
58    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            Self::InvalidSyntax => formatter.write_str("invalid BigInt syntax"),
61            Self::InvalidRadix(radix) => {
62                write!(
63                    formatter,
64                    "invalid BigInt radix {radix}; expected 2 through 36"
65                )
66            }
67            Self::DivisionByZero => formatter.write_str("BigInt division by zero"),
68            Self::NegativeExponent => formatter.write_str("BigInt negative exponent"),
69            Self::BigIntTooLarge => formatter.write_str("BigInt is too large"),
70            Self::AllocationTooLarge => formatter.write_str("BigInt is too large to allocate"),
71            Self::ShiftTooLarge => formatter.write_str("BigInt shift is too large"),
72        }
73    }
74}
75
76impl std::error::Error for BigIntError {}
77
78#[derive(Clone, Eq, Hash, PartialEq)]
79enum BigIntRepr {
80    Short(i64),
81    Heap(Rc<BigInt>),
82}
83
84/// An ECMAScript BigInt using the same short/heap split as `QuickJS`.
85///
86/// The representation is private so callers cannot construct a non-normalized
87/// heap value that should have been a short BigInt.
88#[derive(Clone, Eq, Hash, PartialEq)]
89pub struct JsBigInt(BigIntRepr);
90
91impl JsBigInt {
92    /// Test the tagged representation identity observed by QuickJS's raw
93    /// `JSValue` equality shortcuts. Heap values must share the same cell;
94    /// numeric equality alone is deliberately insufficient.
95    #[must_use]
96    pub fn same_representation(&self, other: &Self) -> bool {
97        match (&self.0, &other.0) {
98            (BigIntRepr::Short(left), BigIntRepr::Short(right)) => left == right,
99            (BigIntRepr::Heap(left), BigIntRepr::Heap(right)) => Rc::ptr_eq(left, right),
100            (BigIntRepr::Short(_), BigIntRepr::Heap(_))
101            | (BigIntRepr::Heap(_), BigIntRepr::Short(_)) => false,
102        }
103    }
104
105    /// Construct zero in the short representation.
106    #[must_use]
107    pub const fn zero() -> Self {
108        Self(BigIntRepr::Short(0))
109    }
110
111    /// Construct one in the short representation.
112    #[must_use]
113    pub const fn one() -> Self {
114        Self(BigIntRepr::Short(1))
115    }
116
117    /// Construct and normalize a `num_bigint::BigInt`.
118    ///
119    /// # Errors
120    ///
121    /// Returns [`BigIntError::BigIntTooLarge`] when the signed limb
122    /// representation exceeds the `QuickJS` allocation limit.
123    pub fn from_bigint(value: BigInt) -> Result<Self, BigIntError> {
124        if signed_limb_len(&value) > MAX_BIGINT_LIMBS {
125            return Err(BigIntError::BigIntTooLarge);
126        }
127        Ok(Self::normalize(value))
128    }
129
130    /// Parse plain decimal text (an optional `-` and digits) with no size
131    /// limit: a value another engine already holds, which may be one limb
132    /// past the allocation guard as `js_bigint_extend` leaves it.
133    pub(crate) fn from_decimal_unchecked(text: &str) -> Option<Self> {
134        BigInt::parse_bytes(text.as_bytes(), 10).map(Self::normalize)
135    }
136
137    /// Convert an exactly integral finite binary64 value to a BigInt.
138    ///
139    /// This is the Number-only branch of QuickJS's BigInt constructor. The
140    /// caller retains responsibility for selecting the distinct non-integer
141    /// and non-finite JavaScript errors.
142    #[must_use]
143    pub fn from_integral_f64(value: f64) -> Option<Self> {
144        if !value.is_finite() || value.fract() != 0.0 {
145            return None;
146        }
147        BigInt::from_f64(value).map(Self::normalize)
148    }
149
150    /// Parse the string form accepted by the ECMAScript `BigInt` constructor.
151    ///
152    /// Leading and trailing ECMAScript whitespace is ignored. The empty string
153    /// is zero. Decimal text may have a leading `+` or `-`; unsigned `0b`, `0o`,
154    /// and `0x` prefixes are accepted. Numeric separators and a trailing `n`
155    /// are deliberately rejected, as they are not part of `StringToBigInt`.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`BigIntError::InvalidSyntax`] for malformed input and an
160    /// allocation-size error for values beyond the upstream limit.
161    pub fn parse_js_string(source: &str) -> Result<Self, BigIntError> {
162        let source = source.trim_matches(|character: char| {
163            u16::try_from(character).is_ok_and(is_ecmascript_whitespace)
164        });
165        if source.is_empty() {
166            return Ok(Self::zero());
167        }
168
169        let bytes = source.as_bytes();
170        match bytes[0] {
171            b'+' | b'-' => {
172                let negative = bytes[0] == b'-';
173                let digits = &source[1..];
174                // Signed non-decimal prefixes are rejected by StringToBigInt.
175                parse_digits(digits, 10, negative)
176            }
177            _ if bytes.len() >= 2 && bytes[0] == b'0' => {
178                let radix = match bytes[1] {
179                    b'b' | b'B' => Some(2),
180                    b'o' | b'O' => Some(8),
181                    b'x' | b'X' => Some(16),
182                    _ => None,
183                };
184                if let Some(radix) = radix {
185                    parse_digits(&source[2..], radix, false)
186                } else {
187                    parse_digits(source, 10, false)
188                }
189            }
190            _ => parse_digits(source, 10, false),
191        }
192    }
193
194    /// Parse an optionally signed digit sequence in `radix`.
195    ///
196    /// Unlike [`Self::parse_js_string`], this low-level entry point does not
197    /// trim whitespace or recognize a base prefix.
198    ///
199    /// # Errors
200    ///
201    /// Returns [`BigIntError::InvalidRadix`],
202    /// [`BigIntError::InvalidSyntax`], or an allocation-size error.
203    pub fn parse_radix(source: &str, radix: u32) -> Result<Self, BigIntError> {
204        validate_radix(radix)?;
205        let (negative, digits) = match source.as_bytes().first().copied() {
206            Some(b'-') => (true, &source[1..]),
207            Some(b'+') => (false, &source[1..]),
208            _ => (false, source),
209        };
210        parse_digits(digits, radix, negative)
211    }
212
213    /// Format the integer with lowercase digits in `radix`.
214    ///
215    /// # Errors
216    ///
217    /// Returns [`BigIntError::InvalidRadix`] unless `radix` is in `2..=36`.
218    pub fn to_string_radix(&self, radix: u32) -> Result<String, BigIntError> {
219        validate_radix(radix)?;
220        Ok(match &self.0 {
221            BigIntRepr::Short(value) => short_to_string_radix(*value, radix),
222            BigIntRepr::Heap(value) => value.to_str_radix(radix),
223        })
224    }
225
226    /// Whether this value uses the `JS_TAG_SHORT_BIG_INT`-like fast path.
227    #[must_use]
228    pub const fn is_short(&self) -> bool {
229        matches!(self.0, BigIntRepr::Short(_))
230    }
231
232    /// Return the immediate value, or `None` for a heap BigInt.
233    #[must_use]
234    pub const fn as_i64(&self) -> Option<i64> {
235        match self.0 {
236            BigIntRepr::Short(value) => Some(value),
237            BigIntRepr::Heap(_) => None,
238        }
239    }
240
241    /// Return a cloned arbitrary-precision value for external integrations.
242    #[must_use]
243    pub fn to_bigint(&self) -> BigInt {
244        match &self.0 {
245            BigIntRepr::Short(value) => BigInt::from(*value),
246            BigIntRepr::Heap(value) => value.as_ref().clone(),
247        }
248    }
249
250    /// Convert this integer to the nearest IEEE-754 binary64 value.
251    ///
252    /// This is the conversion used by the `Number` constructor's `ToNumeric`
253    /// path. It is deliberately separate from ordinary `ToNumber`, which must
254    /// continue to reject BigInt primitives. `num-bigint` retains sticky lower
255    /// bits before the final hardware conversion, yielding round-to-nearest,
256    /// ties-to-even and signed infinity on overflow like pinned QuickJS.
257    #[must_use]
258    pub fn to_f64(&self) -> f64 {
259        match &self.0 {
260            BigIntRepr::Short(value) => *value as f64,
261            BigIntRepr::Heap(value) => value
262                .to_f64()
263                .expect("an arbitrary-precision integer always has an f64 image"),
264        }
265    }
266
267    /// Compare exactly with a Number, as IsLessThan requires (`None` for NaN).
268    #[must_use]
269    pub fn cmp_f64(&self, number: f64) -> Option<Ordering> {
270        const TWO_POW_63: f64 = 9_223_372_036_854_775_808.0;
271        if number.is_nan() {
272            return None;
273        }
274        if (-TWO_POW_63..TWO_POW_63).contains(&number) {
275            let ordering = match &self.0 {
276                BigIntRepr::Short(value) => value.cmp(&(number.trunc() as i64)),
277                // A heap value lies outside i64 and so beyond this Number.
278                BigIntRepr::Heap(value) if value.sign() == Sign::Minus => Ordering::Less,
279                BigIntRepr::Heap(_) => Ordering::Greater,
280            };
281            return Some(ordering.then(0.0.partial_cmp(&number.fract())?));
282        }
283        // |number| >= 2^63 is integral or infinite and beyond every short
284        // value; a heap value of the opposite sign is likewise decided.
285        match &self.0 {
286            BigIntRepr::Heap(value)
287                if number.is_finite() && (value.sign() == Sign::Minus) == (number < 0.0) =>
288            {
289                let number_bits = ((number.to_bits() >> 52) & 0x7ff) - 1022;
290                let magnitude = value.bits().cmp(&number_bits).then_with(|| {
291                    let number = BigUint::from_f64(number.abs()).expect("finite Number");
292                    value.magnitude().cmp(&number)
293                });
294                Some(if number < 0.0 {
295                    magnitude.reverse()
296                } else {
297                    magnitude
298                })
299            }
300            _ => Some(if number > 0.0 {
301                Ordering::Less
302            } else {
303                Ordering::Greater
304            }),
305        }
306    }
307
308    /// The unsigned magnitude bit length (`0` for zero).
309    #[must_use]
310    pub fn magnitude_bits(&self) -> u64 {
311        match &self.0 {
312            BigIntRepr::Short(value) => {
313                let magnitude = value.unsigned_abs();
314                u64::from(u64::BITS - magnitude.leading_zeros())
315            }
316            BigIntRepr::Heap(value) => value.bits(),
317        }
318    }
319
320    /// Number of normalized signed 64-bit limbs used by `QuickJS`.
321    #[must_use]
322    pub fn signed_limb_len(&self) -> u64 {
323        match &self.0 {
324            BigIntRepr::Short(_) => 1,
325            BigIntRepr::Heap(value) => signed_limb_len(value),
326        }
327    }
328
329    pub fn exceeds_allocation_limit(&self) -> bool {
330        self.signed_limb_len() > MAX_BIGINT_LIMBS
331    }
332
333    /// Whether this value is zero.
334    #[must_use]
335    pub fn is_zero(&self) -> bool {
336        match &self.0 {
337            BigIntRepr::Short(value) => *value == 0,
338            BigIntRepr::Heap(value) => value.is_zero(),
339        }
340    }
341
342    /// Whether this value is negative.
343    #[must_use]
344    pub fn is_negative(&self) -> bool {
345        match &self.0 {
346            BigIntRepr::Short(value) => *value < 0,
347            BigIntRepr::Heap(value) => value.sign() == Sign::Minus,
348        }
349    }
350
351    /// Add two BigInts, promoting on short overflow and compacting the result.
352    ///
353    /// # Errors
354    ///
355    /// Returns [`BigIntError::BigIntTooLarge`] if the result exceeds the
356    /// upstream allocation limit.
357    pub fn add(&self, rhs: &Self) -> Result<Self, BigIntError> {
358        let short_result = match (&self.0, &rhs.0) {
359            (BigIntRepr::Short(left), BigIntRepr::Short(right)) => left.checked_add(*right),
360            _ => None,
361        };
362        if let Some(result) = short_result {
363            return Ok(Self::from(result));
364        }
365        self.ensure_operands_allocatable(rhs)?;
366        self.checked_extending_binary(rhs, |left, right| left + right)
367    }
368
369    /// Subtract two BigInts, promoting on short overflow and compacting the
370    /// result.
371    ///
372    /// # Errors
373    ///
374    /// Returns [`BigIntError::BigIntTooLarge`] if the result exceeds the
375    /// upstream allocation limit.
376    pub fn sub(&self, rhs: &Self) -> Result<Self, BigIntError> {
377        let short_result = match (&self.0, &rhs.0) {
378            (BigIntRepr::Short(left), BigIntRepr::Short(right)) => left.checked_sub(*right),
379            _ => None,
380        };
381        if let Some(result) = short_result {
382            return Ok(Self::from(result));
383        }
384        self.ensure_operands_allocatable(rhs)?;
385        self.checked_extending_binary(rhs, |left, right| left - right)
386    }
387
388    /// Apply the pinned QuickJS 2026-06-04 BigInt `--` operation.
389    ///
390    /// QuickJS handles ordinary short values in its VM fast path. At
391    /// `i64::MIN` and for every heap BigInt it enters `js_unary_arith_slow`,
392    /// where unsigned opcode-enum arithmetic turns the intended `-1` limb
393    /// into `UINT32_MAX` before `js_bigint_add`. This observable release quirk
394    /// is deliberately confined to update-decrement; binary subtraction keeps
395    /// mathematical BigInt semantics.
396    pub fn update_decrement(&self) -> Result<Self, BigIntError> {
397        if let BigIntRepr::Short(value) = self.0
398            && value != i64::MIN
399        {
400            return Ok(Self::from(value - 1));
401        }
402        self.add(&Self::from(i64::from(u32::MAX)))
403    }
404
405    /// Multiply two BigInts, promoting on short overflow and compacting the
406    /// result.
407    ///
408    /// # Errors
409    ///
410    /// Returns [`BigIntError::BigIntTooLarge`] if the result exceeds the
411    /// upstream allocation limit.
412    pub fn mul(&self, rhs: &Self) -> Result<Self, BigIntError> {
413        let short_result = match (&self.0, &rhs.0) {
414            (BigIntRepr::Short(left), BigIntRepr::Short(right)) => left.checked_mul(*right),
415            _ => None,
416        };
417        if let Some(result) = short_result {
418            return Ok(Self::from(result));
419        }
420        self.ensure_operands_allocatable(rhs)?;
421        let allocated_limbs = self
422            .signed_limb_len()
423            .checked_add(rhs.signed_limb_len())
424            .ok_or(BigIntError::AllocationTooLarge)?;
425        if allocated_limbs > MAX_BIGINT_LIMBS {
426            return Err(BigIntError::AllocationTooLarge);
427        }
428        self.checked_binary(rhs, |left, right| left * right)
429    }
430
431    /// Divide with truncation toward zero, as required for ECMAScript BigInt.
432    ///
433    /// # Errors
434    ///
435    /// Returns [`BigIntError::DivisionByZero`] when `rhs` is zero.
436    pub fn div(&self, rhs: &Self) -> Result<Self, BigIntError> {
437        if rhs.is_zero() {
438            return Err(BigIntError::DivisionByZero);
439        }
440        let short_result = match (&self.0, &rhs.0) {
441            (BigIntRepr::Short(left), BigIntRepr::Short(right)) => left.checked_div(*right),
442            _ => None,
443        };
444        if let Some(result) = short_result {
445            return Ok(Self::from(result));
446        }
447        if self
448            .signed_limb_len()
449            .checked_add(2)
450            .is_none_or(|allocated_limbs| allocated_limbs > MAX_BIGINT_LIMBS)
451        {
452            return Err(BigIntError::AllocationTooLarge);
453        }
454        // The only overflowing i64 division is MIN / -1. The arbitrary-
455        // precision path naturally promotes it instead of panicking.
456        self.checked_binary(rhs, |left, right| left / right)
457    }
458
459    /// Compute a remainder whose sign follows the dividend.
460    ///
461    /// # Errors
462    ///
463    /// Returns [`BigIntError::DivisionByZero`] when `rhs` is zero.
464    pub fn rem(&self, rhs: &Self) -> Result<Self, BigIntError> {
465        if rhs.is_zero() {
466            return Err(BigIntError::DivisionByZero);
467        }
468        if let (BigIntRepr::Short(left), BigIntRepr::Short(right)) = (&self.0, &rhs.0) {
469            // Rust reports MIN % -1 as overflow even though the mathematical
470            // result is zero.
471            if *left == i64::MIN && *right == -1 {
472                return Ok(Self::zero());
473            }
474            return Ok(Self::from(left % right));
475        }
476        if self
477            .signed_limb_len()
478            .checked_add(2)
479            .is_none_or(|allocated_limbs| allocated_limbs > MAX_BIGINT_LIMBS)
480        {
481            return Err(BigIntError::AllocationTooLarge);
482        }
483        self.checked_binary(rhs, |left, right| left % right)
484    }
485
486    /// Raise this value to a non-negative BigInt exponent.
487    ///
488    /// `0`, `1`, and `-1` retain the same constant-result shortcuts as
489    /// `js_bigint_pow`, even when the exponent itself is too large to fit a
490    /// machine integer.
491    ///
492    /// # Errors
493    ///
494    /// Returns [`BigIntError::NegativeExponent`] for a negative exponent,
495    /// [`BigIntError::BigIntTooLarge`] when the mathematical exponent exceeds
496    /// the upstream limit, and [`BigIntError::AllocationTooLarge`] when an
497    /// intermediate or final allocation crosses the nominal limb guard.
498    pub fn pow(&self, exponent: &Self) -> Result<Self, BigIntError> {
499        if exponent.is_negative() {
500            return Err(BigIntError::NegativeExponent);
501        }
502        if exponent.is_zero() {
503            return Ok(Self::one());
504        }
505        if exponent.is_one() {
506            if self.exceeds_allocation_limit() {
507                return Err(BigIntError::AllocationTooLarge);
508            }
509            return Ok(self.clone());
510        }
511        if self.is_zero() || self.is_one() {
512            return Ok(self.clone());
513        }
514        if self.is_negative_one() {
515            return Ok(if exponent.is_odd() {
516                Self::from(-1)
517            } else {
518                Self::one()
519            });
520        }
521
522        let exponent = exponent
523            .to_u32()
524            .filter(|value| *value <= i32::MAX as u32)
525            .ok_or(BigIntError::BigIntTooLarge)?;
526
527        // js_bigint_pow has an exact-allocation shortcut for one-limb powers
528        // of two. Besides being faster, this is observable at the allocation
529        // boundary because the generic multiplier reserves both input widths.
530        if let Some(base) = self.as_i64() {
531            let magnitude = base.unsigned_abs();
532            if magnitude.is_power_of_two() {
533                let base_shift = u64::from(magnitude.trailing_zeros());
534                let result_shift = u64::from(exponent) * base_shift;
535                if result_shift > MAX_BIGINT_BITS {
536                    return Err(BigIntError::BigIntTooLarge);
537                }
538                let mut result = BigInt::one()
539                    << usize::try_from(result_shift)
540                        .expect("bounded QuickJS BigInt power shift fits usize");
541                if base.is_negative() && exponent & 1 != 0 {
542                    result = -result;
543                }
544                return Self::from_bigint(result).map_err(|error| match error {
545                    BigIntError::BigIntTooLarge => BigIntError::AllocationTooLarge,
546                    error => error,
547                });
548            }
549        }
550
551        // The generic upstream algorithm starts with `a`, then scans the
552        // remaining exponent bits from most to least significant. Matching
553        // that order avoids an observable final `1 * result` allocation near
554        // the 16,384-limb boundary.
555        if self.exceeds_allocation_limit() {
556            return Err(BigIntError::AllocationTooLarge);
557        }
558        let highest_bit = u32::BITS - exponent.leading_zeros();
559        let mut result = self.clone();
560        for bit in (0..highest_bit - 1).rev() {
561            result = result.mul(&result)?;
562            if exponent & (1 << bit) != 0 {
563                result = result.mul(self)?;
564            }
565        }
566        Ok(result)
567    }
568
569    /// Arithmetic negation, promoting `i64::MIN` to a heap BigInt.
570    ///
571    /// # Errors
572    ///
573    /// Returns [`BigIntError::BigIntTooLarge`] if the result exceeds the
574    /// upstream allocation limit.
575    pub fn neg(&self) -> Result<Self, BigIntError> {
576        let short_result = match &self.0 {
577            BigIntRepr::Short(value) => value.checked_neg(),
578            BigIntRepr::Heap(_) => None,
579        };
580        if let Some(result) = short_result {
581            return Ok(Self::from(result));
582        }
583        if self.exceeds_allocation_limit() {
584            return Err(BigIntError::AllocationTooLarge);
585        }
586        let result = -self.as_bigint().as_ref();
587        if signed_limb_len(&result) > MAX_BIGINT_LIMBS + 1 {
588            return Err(BigIntError::AllocationTooLarge);
589        }
590        Ok(Self::normalize(result))
591    }
592
593    /// Infinite-width two's-complement bitwise AND.
594    ///
595    /// # Errors
596    ///
597    /// Returns [`BigIntError::AllocationTooLarge`] when either operand came
598    /// from QuickJS's unguarded one-sign-limb extension.
599    pub fn bit_and(&self, rhs: &Self) -> Result<Self, BigIntError> {
600        if let (BigIntRepr::Short(left), BigIntRepr::Short(right)) = (&self.0, &rhs.0) {
601            return Ok(Self::from(left & right));
602        }
603        self.ensure_operands_allocatable(rhs)?;
604        Ok(self.unchecked_binary(rhs, |left, right| left & right))
605    }
606
607    /// Infinite-width two's-complement bitwise OR.
608    ///
609    /// # Errors
610    ///
611    /// Returns [`BigIntError::AllocationTooLarge`] when either operand came
612    /// from QuickJS's unguarded one-sign-limb extension.
613    pub fn bit_or(&self, rhs: &Self) -> Result<Self, BigIntError> {
614        if let (BigIntRepr::Short(left), BigIntRepr::Short(right)) = (&self.0, &rhs.0) {
615            return Ok(Self::from(left | right));
616        }
617        self.ensure_operands_allocatable(rhs)?;
618        Ok(self.unchecked_binary(rhs, |left, right| left | right))
619    }
620
621    /// Infinite-width two's-complement bitwise XOR.
622    ///
623    /// # Errors
624    ///
625    /// Returns [`BigIntError::AllocationTooLarge`] when either operand came
626    /// from QuickJS's unguarded one-sign-limb extension.
627    pub fn bit_xor(&self, rhs: &Self) -> Result<Self, BigIntError> {
628        if let (BigIntRepr::Short(left), BigIntRepr::Short(right)) = (&self.0, &rhs.0) {
629            return Ok(Self::from(left ^ right));
630        }
631        self.ensure_operands_allocatable(rhs)?;
632        Ok(self.unchecked_binary(rhs, |left, right| left ^ right))
633    }
634
635    /// Infinite-width two's-complement bitwise NOT.
636    ///
637    /// # Errors
638    ///
639    /// Returns [`BigIntError::AllocationTooLarge`] when the operand came from
640    /// QuickJS's unguarded one-sign-limb extension.
641    pub fn bit_not(&self) -> Result<Self, BigIntError> {
642        if self.exceeds_allocation_limit() {
643            return Err(BigIntError::AllocationTooLarge);
644        }
645        Ok(match &self.0 {
646            BigIntRepr::Short(value) => Self::from(!value),
647            BigIntRepr::Heap(value) => Self::normalize(!value.as_ref()),
648        })
649    }
650
651    /// ECMAScript BigInt left shift. A negative count shifts right.
652    ///
653    /// # Errors
654    ///
655    /// Returns [`BigIntError::ShiftTooLarge`] when a non-zero left-shift result
656    /// would exceed the `QuickJS` allocation limit.
657    pub fn shl(&self, count: &Self) -> Result<Self, BigIntError> {
658        self.shift(count, true)
659    }
660
661    /// ECMAScript BigInt arithmetic right shift. A negative count shifts left.
662    ///
663    /// # Errors
664    ///
665    /// Returns [`BigIntError::ShiftTooLarge`] when a reversed (left) shift
666    /// would exceed the `QuickJS` allocation limit.
667    pub fn shr(&self, count: &Self) -> Result<Self, BigIntError> {
668        self.shift(count, false)
669    }
670
671    /// `QuickJS` 2026-06-04 behavior for `BigInt.asIntN(bits, value)`.
672    ///
673    /// # Errors
674    ///
675    /// Returns [`BigIntError::AllocationTooLarge`] when truncation would
676    /// allocate the 16,385th nominal limb. The pinned release still returns
677    /// an already-extended input unchanged once `bits` reaches its storage
678    /// width, so that early return remains deliberately ahead of the guard.
679    pub fn as_int_n(&self, bits: u64) -> Result<Self, BigIntError> {
680        self.as_n(bits, true)
681    }
682
683    /// `QuickJS` 2026-06-04 behavior for `BigInt.asUintN(bits, value)`.
684    ///
685    /// This intentionally preserves the upstream signed-limb behavior instead
686    /// of silently substituting the ECMA-262 result. In this release, a short
687    /// negative value is returned unchanged for `bits >= 64`; heap truncation
688    /// at a multiple of 64 can likewise produce a negative value. These are
689    /// observable compatibility requirements for the selected baseline.
690    ///
691    /// # Errors
692    ///
693    /// Returns [`BigIntError::AllocationTooLarge`] at the same guarded
694    /// truncation boundary as [`Self::as_int_n`].
695    pub fn as_uint_n(&self, bits: u64) -> Result<Self, BigIntError> {
696        self.as_n(bits, false)
697    }
698
699    fn normalize(value: BigInt) -> Self {
700        if let Some(value) = value.to_i64() {
701            Self(BigIntRepr::Short(value))
702        } else {
703            Self(BigIntRepr::Heap(Rc::new(value)))
704        }
705    }
706
707    fn as_bigint(&self) -> Cow<'_, BigInt> {
708        match &self.0 {
709            BigIntRepr::Short(value) => Cow::Owned(BigInt::from(*value)),
710            BigIntRepr::Heap(value) => Cow::Borrowed(value.as_ref()),
711        }
712    }
713
714    fn checked_binary(
715        &self,
716        rhs: &Self,
717        operation: impl FnOnce(&BigInt, &BigInt) -> BigInt,
718    ) -> Result<Self, BigIntError> {
719        let left = self.as_bigint();
720        let right = rhs.as_bigint();
721        Self::from_bigint(operation(left.as_ref(), right.as_ref())).map_err(|error| match error {
722            BigIntError::BigIntTooLarge => BigIntError::AllocationTooLarge,
723            error => error,
724        })
725    }
726
727    fn checked_extending_binary(
728        &self,
729        rhs: &Self,
730        operation: impl FnOnce(&BigInt, &BigInt) -> BigInt,
731    ) -> Result<Self, BigIntError> {
732        let left = self.as_bigint();
733        let right = rhs.as_bigint();
734        let result = operation(left.as_ref(), right.as_ref());
735        if signed_limb_len(&result) > MAX_BIGINT_LIMBS + 1 {
736            return Err(BigIntError::AllocationTooLarge);
737        }
738        Ok(Self::normalize(result))
739    }
740
741    fn ensure_operands_allocatable(&self, rhs: &Self) -> Result<(), BigIntError> {
742        if self.exceeds_allocation_limit() || rhs.exceeds_allocation_limit() {
743            Err(BigIntError::AllocationTooLarge)
744        } else {
745            Ok(())
746        }
747    }
748
749    fn unchecked_binary(
750        &self,
751        rhs: &Self,
752        operation: impl FnOnce(&BigInt, &BigInt) -> BigInt,
753    ) -> Self {
754        let left = self.as_bigint();
755        let right = rhs.as_bigint();
756        let result = operation(left.as_ref(), right.as_ref());
757        debug_assert!(signed_limb_len(&result) <= MAX_BIGINT_LIMBS);
758        Self::normalize(result)
759    }
760
761    fn is_one(&self) -> bool {
762        matches!(self.0, BigIntRepr::Short(1))
763    }
764
765    fn is_negative_one(&self) -> bool {
766        matches!(self.0, BigIntRepr::Short(-1))
767    }
768
769    fn is_odd(&self) -> bool {
770        match &self.0 {
771            BigIntRepr::Short(value) => value & 1 != 0,
772            BigIntRepr::Heap(value) => value.magnitude().bit(0),
773        }
774    }
775
776    fn to_u32(&self) -> Option<u32> {
777        match &self.0 {
778            BigIntRepr::Short(value) => u32::try_from(*value).ok(),
779            BigIntRepr::Heap(value) => value.to_u32(),
780        }
781    }
782
783    fn shift(&self, count: &Self, operator_shifts_left: bool) -> Result<Self, BigIntError> {
784        let shifts_left = operator_shifts_left != count.is_negative();
785        let count = count
786            .magnitude_to_u64()
787            .unwrap_or(u64::MAX)
788            .min(i32::MAX as u64);
789        if shifts_left {
790            // js_bigint_shl returns zero before attempting an allocation.
791            if self.is_zero() {
792                return Ok(Self::zero());
793            }
794            let allocated_limbs = self
795                .signed_limb_len()
796                .checked_add(count / BIGINT_LIMB_BITS)
797                .ok_or(BigIntError::ShiftTooLarge)?;
798            if allocated_limbs > MAX_BIGINT_LIMBS {
799                return Err(BigIntError::ShiftTooLarge);
800            }
801            let count = usize::try_from(count).map_err(|_| BigIntError::ShiftTooLarge)?;
802            let result = self.as_bigint().as_ref() << count;
803            if signed_limb_len(&result) > MAX_BIGINT_LIMBS + 1 {
804                return Err(BigIntError::ShiftTooLarge);
805            }
806            Ok(Self::normalize(result))
807        } else {
808            let limb_offset = count / BIGINT_LIMB_BITS;
809            if limb_offset >= self.signed_limb_len() {
810                return Ok(self.right_shift_saturation());
811            }
812            if self.signed_limb_len() - limb_offset > MAX_BIGINT_LIMBS {
813                return Err(BigIntError::ShiftTooLarge);
814            }
815            let count = usize::try_from(count).expect("bounded BigInt shift fits usize");
816            Ok(Self::normalize(self.as_bigint().as_ref() >> count))
817        }
818    }
819
820    fn right_shift_saturation(&self) -> Self {
821        if self.is_negative() {
822            Self::from(-1)
823        } else {
824            Self::zero()
825        }
826    }
827
828    fn magnitude_to_u64(&self) -> Option<u64> {
829        match &self.0 {
830            BigIntRepr::Short(value) => Some(value.unsigned_abs()),
831            BigIntRepr::Heap(value) => value.magnitude().to_u64(),
832        }
833    }
834
835    fn as_n(&self, bits: u64, signed: bool) -> Result<Self, BigIntError> {
836        if bits == 0 {
837            return Ok(Self::zero());
838        }
839
840        // This early return is representation-dependent in upstream. In
841        // particular, asUintN(128, -1n) returns -1n in QuickJS 2026-06-04.
842        let storage_bits = self.signed_limb_len() * BIGINT_LIMB_BITS;
843        if bits >= storage_bits {
844            return Ok(self.clone());
845        }
846
847        let allocated_limbs = bits
848            .checked_add(BIGINT_LIMB_BITS - 1)
849            .ok_or(BigIntError::AllocationTooLarge)?
850            / BIGINT_LIMB_BITS;
851        if allocated_limbs > MAX_BIGINT_LIMBS {
852            return Err(BigIntError::AllocationTooLarge);
853        }
854
855        // At this point bits is within the nominal allocation guard.
856        let bits_usize = usize::try_from(bits).expect("bounded BigInt bit width fits usize");
857        let modulus = BigInt::one() << bits_usize;
858        let mut truncated = self.as_bigint().as_ref() % &modulus;
859        if truncated.sign() == Sign::Minus {
860            truncated += &modulus;
861        }
862
863        // js_bigint_asUintN writes a whole signed limb when bits is a multiple
864        // of JS_LIMB_BITS. If that limb's top bit is set, normalization observes
865        // a negative result even for asUintN. Preserve that release behavior.
866        let interpret_as_signed = signed || bits & (BIGINT_LIMB_BITS - 1) == 0;
867        if interpret_as_signed && truncated.bit(bits - 1) {
868            truncated -= modulus;
869        }
870        Ok(Self::normalize(truncated))
871    }
872}
873
874impl Default for JsBigInt {
875    fn default() -> Self {
876        Self::zero()
877    }
878}
879
880impl From<i64> for JsBigInt {
881    fn from(value: i64) -> Self {
882        Self(BigIntRepr::Short(value))
883    }
884}
885
886impl From<i32> for JsBigInt {
887    fn from(value: i32) -> Self {
888        Self::from(i64::from(value))
889    }
890}
891
892impl From<u32> for JsBigInt {
893    fn from(value: u32) -> Self {
894        Self::from(i64::from(value))
895    }
896}
897
898impl From<u64> for JsBigInt {
899    fn from(value: u64) -> Self {
900        if let Ok(value) = i64::try_from(value) {
901            Self::from(value)
902        } else {
903            Self::normalize(BigInt::from(value))
904        }
905    }
906}
907
908impl From<i128> for JsBigInt {
909    fn from(value: i128) -> Self {
910        Self::normalize(BigInt::from(value))
911    }
912}
913
914impl From<u128> for JsBigInt {
915    fn from(value: u128) -> Self {
916        Self::normalize(BigInt::from(value))
917    }
918}
919
920impl TryFrom<BigInt> for JsBigInt {
921    type Error = BigIntError;
922
923    fn try_from(value: BigInt) -> Result<Self, Self::Error> {
924        Self::from_bigint(value)
925    }
926}
927
928impl FromStr for JsBigInt {
929    type Err = BigIntError;
930
931    fn from_str(source: &str) -> Result<Self, Self::Err> {
932        Self::parse_js_string(source)
933    }
934}
935
936impl Ord for JsBigInt {
937    fn cmp(&self, other: &Self) -> Ordering {
938        match (&self.0, &other.0) {
939            (BigIntRepr::Short(left), BigIntRepr::Short(right)) => left.cmp(right),
940            (BigIntRepr::Heap(left), BigIntRepr::Heap(right)) => left.cmp(right),
941            // A normalized heap value lies outside i64, so its sign decides.
942            (BigIntRepr::Short(_), BigIntRepr::Heap(heap)) => match heap.sign() {
943                Sign::Minus => Ordering::Greater,
944                _ => Ordering::Less,
945            },
946            (BigIntRepr::Heap(heap), BigIntRepr::Short(_)) => match heap.sign() {
947                Sign::Minus => Ordering::Less,
948                _ => Ordering::Greater,
949            },
950        }
951    }
952}
953
954impl PartialOrd for JsBigInt {
955    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
956        Some(self.cmp(other))
957    }
958}
959
960impl fmt::Display for JsBigInt {
961    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
962        match &self.0 {
963            BigIntRepr::Short(value) => value.fmt(formatter),
964            BigIntRepr::Heap(value) => value.fmt(formatter),
965        }
966    }
967}
968
969impl fmt::Debug for JsBigInt {
970    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
971        match &self.0 {
972            BigIntRepr::Short(value) => formatter.debug_tuple("ShortBigInt").field(value).finish(),
973            BigIntRepr::Heap(value) => formatter.debug_tuple("BigInt").field(value).finish(),
974        }
975    }
976}
977
978fn validate_radix(radix: u32) -> Result<(), BigIntError> {
979    if (2..=36).contains(&radix) {
980        Ok(())
981    } else {
982        Err(BigIntError::InvalidRadix(radix))
983    }
984}
985
986fn parse_digits(digits: &str, radix: u32, negative: bool) -> Result<JsBigInt, BigIntError> {
987    validate_radix(radix)?;
988    if digits.is_empty()
989        || !digits
990            .as_bytes()
991            .iter()
992            .all(|byte| is_radix_digit(*byte, radix))
993    {
994        return Err(BigIntError::InvalidSyntax);
995    }
996
997    // js_bigint_from_string discards leading zeroes before its conservative
998    // source-length guard, so an arbitrarily long spelling of zero stays zero.
999    let significant_digits = digits.trim_start_matches('0');
1000    if significant_digits.is_empty() {
1001        return Ok(JsBigInt::zero());
1002    }
1003    let digit_count = u64::try_from(significant_digits.len()).unwrap_or(u64::MAX);
1004    if digit_count > MAX_BIGINT_BITS {
1005        return Err(BigIntError::AllocationTooLarge);
1006    }
1007
1008    // `js_bigint_from_string` allocates from a conservative source-width
1009    // estimate before it parses any digits. The extra sign limb makes an
1010    // estimate of exactly MAX_BIGINT_BITS fail even when the normalized value
1011    // would fit in 16,384 limbs. This is observable at the first failing
1012    // lengths for bases 2, 8, 10 and 16, so checking only the final value's
1013    // signed limb length is insufficient.
1014    let log2_radix = u64::from(u32::BITS - (radix - 1).leading_zeros());
1015    let estimated_bits = if radix == 10 {
1016        (digit_count * 27).div_ceil(8)
1017    } else {
1018        digit_count * log2_radix
1019    };
1020    let estimated_limbs = estimated_bits / BIGINT_LIMB_BITS + 1;
1021    if estimated_limbs > MAX_BIGINT_LIMBS {
1022        return Err(BigIntError::AllocationTooLarge);
1023    }
1024
1025    let magnitude = BigUint::parse_bytes(significant_digits.as_bytes(), radix)
1026        .ok_or(BigIntError::InvalidSyntax)?;
1027    let sign = if negative && !magnitude.is_zero() {
1028        Sign::Minus
1029    } else if magnitude.is_zero() {
1030        Sign::NoSign
1031    } else {
1032        Sign::Plus
1033    };
1034    JsBigInt::from_bigint(BigInt::from_biguint(sign, magnitude))
1035}
1036
1037fn is_radix_digit(byte: u8, radix: u32) -> bool {
1038    let value = match byte {
1039        b'0'..=b'9' => u32::from(byte - b'0'),
1040        b'a'..=b'z' => u32::from(byte - b'a') + 10,
1041        b'A'..=b'Z' => u32::from(byte - b'A') + 10,
1042        _ => return false,
1043    };
1044    value < radix
1045}
1046
1047fn short_to_string_radix(value: i64, radix: u32) -> String {
1048    const DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
1049
1050    if value == 0 {
1051        return "0".to_owned();
1052    }
1053
1054    let negative = value < 0;
1055    let mut magnitude = value.unsigned_abs();
1056    let radix = u64::from(radix);
1057    let mut reversed = Vec::with_capacity(65);
1058    while magnitude != 0 {
1059        let digit = usize::try_from(magnitude % radix).expect("radix digit fits usize");
1060        reversed.push(DIGITS[digit]);
1061        magnitude /= radix;
1062    }
1063    if negative {
1064        reversed.push(b'-');
1065    }
1066    reversed.reverse();
1067    String::from_utf8(reversed).expect("BigInt formatting only emits ASCII")
1068}
1069
1070fn signed_limb_len(value: &BigInt) -> u64 {
1071    let signed_bits = match value.sign() {
1072        Sign::NoSign => 1,
1073        Sign::Plus => value.bits() + 1,
1074        // -m needs bits(m - 1) + 1 two's-complement bits; m - 1 only loses a
1075        // bit when m is a power of two, so no copy of the magnitude is needed.
1076        Sign::Minus => {
1077            let magnitude = value.magnitude();
1078            let bits = magnitude.bits();
1079            if magnitude.trailing_zeros() == Some(bits - 1) {
1080                bits
1081            } else {
1082                bits + 1
1083            }
1084        }
1085    };
1086    signed_bits.div_ceil(BIGINT_LIMB_BITS).max(1)
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091    use super::{BIGINT_LIMB_BITS, BigIntError, JsBigInt, MAX_BIGINT_BITS, MAX_BIGINT_LIMBS};
1092
1093    fn bigint(source: &str) -> JsBigInt {
1094        JsBigInt::parse_js_string(source).unwrap()
1095    }
1096
1097    #[test]
1098    fn normalizes_short_values_and_promotes_overflow() {
1099        let max = JsBigInt::from(i64::MAX);
1100        let min = JsBigInt::from(i64::MIN);
1101        assert!(max.is_short());
1102        assert!(min.is_short());
1103
1104        let above_max = max.add(&JsBigInt::one()).unwrap();
1105        let below_min = min.sub(&JsBigInt::one()).unwrap();
1106        assert!(!above_max.is_short());
1107        assert!(!below_min.is_short());
1108        assert!(max < above_max && min < above_max && below_min < min && below_min < max);
1109        assert!(above_max > max && below_min < above_max && max > below_min);
1110        assert_eq!(above_max.to_string(), "9223372036854775808");
1111        assert_eq!(below_min.to_string(), "-9223372036854775809");
1112
1113        assert_eq!(above_max.sub(&JsBigInt::one()).unwrap(), max);
1114        assert_eq!(below_min.add(&JsBigInt::one()).unwrap(), min);
1115        assert!(above_max.sub(&JsBigInt::one()).unwrap().is_short());
1116    }
1117
1118    #[test]
1119    fn update_decrement_preserves_pinned_unsigned_opcode_quirk() {
1120        assert_eq!(
1121            JsBigInt::from(4).update_decrement().unwrap(),
1122            JsBigInt::from(3)
1123        );
1124        assert_eq!(
1125            JsBigInt::from(i64::MIN).update_decrement().unwrap(),
1126            bigint("-9223372032559808513")
1127        );
1128        assert_eq!(
1129            bigint("9223372036854775808").update_decrement().unwrap(),
1130            bigint("9223372041149743103")
1131        );
1132        assert_eq!(
1133            bigint("-9223372036854775809").update_decrement().unwrap(),
1134            bigint("-9223372032559808514")
1135        );
1136        assert_eq!(
1137            bigint("9223372036854775808").sub(&JsBigInt::one()).unwrap(),
1138            JsBigInt::from(i64::MAX)
1139        );
1140    }
1141
1142    #[test]
1143    fn signed_limb_width_matches_quickjs_twos_complement_normalization() {
1144        assert_eq!(JsBigInt::from(i64::MAX).signed_limb_len(), 1);
1145        assert_eq!(JsBigInt::from(i64::MIN).signed_limb_len(), 1);
1146        assert_eq!(bigint("9223372036854775808").signed_limb_len(), 2);
1147        assert_eq!(bigint("-9223372036854775809").signed_limb_len(), 2);
1148        assert_eq!(
1149            bigint("170141183460469231731687303715884105727").signed_limb_len(),
1150            2
1151        );
1152        assert_eq!(
1153            bigint("170141183460469231731687303715884105728").signed_limb_len(),
1154            3
1155        );
1156        assert_eq!(
1157            bigint("-170141183460469231731687303715884105728").signed_limb_len(),
1158            2
1159        );
1160        assert_eq!(
1161            bigint("-170141183460469231731687303715884105729").signed_limb_len(),
1162            3
1163        );
1164    }
1165
1166    #[test]
1167    fn copy_free_signed_limb_len_matches_decremented_magnitude_formula() {
1168        use num_bigint::{BigInt, Sign};
1169        fn reference(value: &BigInt) -> u64 {
1170            let bits = match value.sign() {
1171                Sign::NoSign => 1,
1172                Sign::Plus => value.bits() + 1,
1173                Sign::Minus => (value.magnitude() - 1u8).bits() + 1,
1174            };
1175            bits.div_ceil(BIGINT_LIMB_BITS).max(1)
1176        }
1177        let mut samples = vec![BigInt::from(0)];
1178        for shift in 0..300_u32 {
1179            let power = BigInt::from(1) << shift;
1180            samples.extend([&power - 1, power.clone(), &power + 1]);
1181        }
1182        let mut state = BigInt::from(0x9e37_79b9_7f4a_7c15_u64);
1183        for _ in 0..200 {
1184            state = (&state * 6_364_136_223_846_793_005_u64 + 1_442_695_040_888_963_407_u64)
1185                % (BigInt::from(1) << 400_u32);
1186            samples.push(&state >> (state.bits() % 397));
1187        }
1188        for magnitude in samples {
1189            for value in [magnitude.clone(), -magnitude] {
1190                assert_eq!(super::signed_limb_len(&value), reference(&value), "{value}");
1191            }
1192        }
1193    }
1194
1195    #[test]
1196    fn cmp_f64_matches_exact_arbitrary_precision_comparison() {
1197        use num_bigint::BigInt;
1198        use num_traits::FromPrimitive;
1199        use std::cmp::Ordering;
1200        fn reference(left: &JsBigInt, number: f64) -> Option<Ordering> {
1201            if number.is_nan() {
1202                return None;
1203            }
1204            if number.is_infinite() {
1205                return Some(if number > 0.0 {
1206                    Ordering::Less
1207                } else {
1208                    Ordering::Greater
1209                });
1210            }
1211            let truncated = BigInt::from_f64(number.trunc()).unwrap();
1212            Some(
1213                left.to_bigint()
1214                    .cmp(&truncated)
1215                    .then(0.0.partial_cmp(&number.fract()).unwrap()),
1216            )
1217        }
1218        let mut values = vec![JsBigInt::from(i64::MIN), JsBigInt::from(i64::MAX)];
1219        for text in [
1220            "0",
1221            "1",
1222            "9007199254740993",
1223            "9223372036854775808",
1224            "1000000000000000000000000000000",
1225        ] {
1226            for sign in ["", "-"] {
1227                let value = bigint(&format!("{sign}{text}"));
1228                values.extend([value.sub(&JsBigInt::one()).unwrap(), value.clone()]);
1229                values.push(value.add(&JsBigInt::one()).unwrap());
1230            }
1231        }
1232        let mut numbers = vec![
1233            f64::NAN,
1234            f64::INFINITY,
1235            -0.0,
1236            0.5,
1237            1.5,
1238            1e20,
1239            1e30,
1240            f64::MAX,
1241        ];
1242        numbers.extend([2f64.powi(53), 2f64.powi(63), 2f64.powi(64), 2f64.powi(99)]);
1243        numbers.extend(values.iter().map(JsBigInt::to_f64));
1244        for number in numbers.clone() {
1245            let next = f64::from_bits(number.to_bits() + 1);
1246            numbers.extend([next, f64::from_bits(number.to_bits().saturating_sub(1))]);
1247        }
1248        for value in &values {
1249            for number in numbers.iter().flat_map(|number| [*number, -*number]) {
1250                assert_eq!(
1251                    value.cmp_f64(number),
1252                    reference(value, number),
1253                    "{value} vs {number}"
1254                );
1255            }
1256        }
1257    }
1258
1259    #[test]
1260    fn parses_string_to_bigint_forms_and_formats_all_radices() {
1261        assert_eq!(bigint(""), JsBigInt::zero());
1262        assert_eq!(bigint("\u{00a0}  +123 \u{feff}"), JsBigInt::from(123));
1263        assert_eq!(bigint("-00123"), JsBigInt::from(-123));
1264        assert_eq!(bigint("0b101010"), JsBigInt::from(42));
1265        assert_eq!(bigint("0O77"), JsBigInt::from(63));
1266        assert_eq!(bigint("0xabcdef"), JsBigInt::from(0x00ab_cdef));
1267
1268        let large = bigint("1267650600228229401496703205376");
1269        assert_eq!(
1270            large.to_string_radix(10).unwrap(),
1271            "1267650600228229401496703205376"
1272        );
1273        assert_eq!(
1274            large.to_string_radix(8).unwrap(),
1275            "2000000000000000000000000000000000"
1276        );
1277        assert_eq!(
1278            large.to_string_radix(16).unwrap(),
1279            "10000000000000000000000000"
1280        );
1281        assert_eq!(
1282            bigint("-1267650600228229401496703205376")
1283                .to_string_radix(36)
1284                .unwrap(),
1285            "-3ewfdnca0n6ld1ggvfgg"
1286        );
1287
1288        assert_eq!(JsBigInt::parse_radix("z", 36).unwrap(), JsBigInt::from(35));
1289        assert_eq!(
1290            JsBigInt::parse_radix("+ff", 16).unwrap(),
1291            JsBigInt::from(255)
1292        );
1293        assert_eq!(
1294            JsBigInt::parse_radix("-ff", 16).unwrap(),
1295            JsBigInt::from(-255)
1296        );
1297    }
1298
1299    #[test]
1300    fn parsing_preserves_the_upstream_conservative_allocation_guard() {
1301        for (prefix, digit, first_failing_length) in [
1302            ("0b", '1', 1_048_576),
1303            ("0o", '7', 349_526),
1304            ("", '9', 310_689),
1305            ("0x", 'f', 262_144),
1306        ] {
1307            let source = format!("{prefix}{}", digit.to_string().repeat(first_failing_length));
1308            assert_eq!(
1309                JsBigInt::parse_js_string(&source),
1310                Err(BigIntError::AllocationTooLarge),
1311                "radix spelling starting with {prefix:?} crossed the wrong allocation boundary"
1312            );
1313        }
1314
1315        let leading_zeroes = format!("0x{}1", "0".repeat(262_144));
1316        assert_eq!(
1317            JsBigInt::parse_js_string(&leading_zeroes).unwrap(),
1318            JsBigInt::one(),
1319            "leading zeroes must be discarded before the conservative guard"
1320        );
1321    }
1322
1323    #[test]
1324    fn rejects_invalid_string_to_bigint_forms() {
1325        for source in ["+", "-", "-0x1", "+0x1", "0x", "1_0", "1n", "\0a", " 12 r "] {
1326            assert_eq!(
1327                JsBigInt::parse_js_string(source),
1328                Err(BigIntError::InvalidSyntax)
1329            );
1330        }
1331        assert_eq!(
1332            JsBigInt::parse_radix("2", 2),
1333            Err(BigIntError::InvalidSyntax)
1334        );
1335        assert_eq!(
1336            JsBigInt::parse_radix("1", 1),
1337            Err(BigIntError::InvalidRadix(1))
1338        );
1339        assert_eq!(
1340            JsBigInt::one().to_string_radix(37),
1341            Err(BigIntError::InvalidRadix(37))
1342        );
1343    }
1344
1345    #[test]
1346    fn arithmetic_matches_upstream_large_integer_vectors() {
1347        let three_to_100 = JsBigInt::from(3).pow(&JsBigInt::from(100)).unwrap();
1348        assert_eq!(
1349            three_to_100.to_string(),
1350            "515377520732011331036461129765621272702107522001"
1351        );
1352
1353        let dividend = bigint("3213213213213213432453243");
1354        let divisor = bigint("123434343439");
1355        assert_eq!(
1356            dividend.div(&divisor).unwrap().to_string(),
1357            "26031760073331"
1358        );
1359        assert_eq!(dividend.rem(&divisor).unwrap().to_string(), "26953727934");
1360        let negative = dividend.neg().unwrap();
1361        assert_eq!(
1362            negative.div(&divisor).unwrap().to_string(),
1363            "-26031760073331"
1364        );
1365        assert_eq!(negative.rem(&divisor).unwrap().to_string(), "-26953727934");
1366
1367        assert_eq!(
1368            JsBigInt::from(-2)
1369                .pow(&JsBigInt::from(127))
1370                .unwrap()
1371                .to_string(),
1372            "-170141183460469231731687303715884105728"
1373        );
1374        assert_eq!(
1375            JsBigInt::from(7)
1376                .pow(&JsBigInt::from(20))
1377                .unwrap()
1378                .to_string(),
1379            "79792266297612001"
1380        );
1381    }
1382
1383    #[test]
1384    fn division_promotes_min_overflow_and_reports_zero() {
1385        let promoted = JsBigInt::from(i64::MIN).div(&JsBigInt::from(-1)).unwrap();
1386        assert_eq!(promoted.to_string(), "9223372036854775808");
1387        assert!(!promoted.is_short());
1388        assert_eq!(
1389            JsBigInt::from(i64::MIN).rem(&JsBigInt::from(-1)).unwrap(),
1390            JsBigInt::zero()
1391        );
1392        assert_eq!(
1393            JsBigInt::one().div(&JsBigInt::zero()),
1394            Err(BigIntError::DivisionByZero)
1395        );
1396        assert_eq!(
1397            JsBigInt::one().rem(&JsBigInt::zero()),
1398            Err(BigIntError::DivisionByZero)
1399        );
1400    }
1401
1402    #[test]
1403    fn pow_handles_sign_shortcuts_and_explicit_errors() {
1404        assert_eq!(
1405            JsBigInt::from(123).pow(&JsBigInt::zero()).unwrap(),
1406            JsBigInt::one()
1407        );
1408        assert_eq!(
1409            JsBigInt::from(-1)
1410                .pow(&bigint("999999999999999999999999999999999999999"))
1411                .unwrap(),
1412            JsBigInt::from(-1)
1413        );
1414        assert_eq!(
1415            JsBigInt::from(2).pow(&JsBigInt::from(-1)),
1416            Err(BigIntError::NegativeExponent)
1417        );
1418        assert_eq!(
1419            JsBigInt::from(2).pow(&bigint("2147483648")),
1420            Err(BigIntError::BigIntTooLarge)
1421        );
1422
1423        let half_width_base = JsBigInt::one()
1424            .shl(&JsBigInt::from(MAX_BIGINT_BITS / 2 - 2))
1425            .unwrap()
1426            .add(&JsBigInt::one())
1427            .unwrap();
1428        assert_eq!(
1429            half_width_base.pow(&JsBigInt::from(2)).unwrap(),
1430            half_width_base.mul(&half_width_base).unwrap()
1431        );
1432
1433        let max_power_of_two = JsBigInt::from(2)
1434            .pow(&JsBigInt::from(MAX_BIGINT_BITS - 2))
1435            .unwrap();
1436        assert_eq!(max_power_of_two.signed_limb_len(), MAX_BIGINT_LIMBS);
1437        assert_eq!(
1438            JsBigInt::from(2).pow(&JsBigInt::from(MAX_BIGINT_BITS - 1)),
1439            Err(BigIntError::AllocationTooLarge)
1440        );
1441        assert_eq!(
1442            JsBigInt::from(-2).pow(&JsBigInt::from(MAX_BIGINT_BITS)),
1443            Err(BigIntError::AllocationTooLarge)
1444        );
1445        assert_eq!(
1446            JsBigInt::from(2).pow(&JsBigInt::from(MAX_BIGINT_BITS + 1)),
1447            Err(BigIntError::BigIntTooLarge)
1448        );
1449        assert!(
1450            JsBigInt::from(-2)
1451                .pow(&JsBigInt::from(MAX_BIGINT_BITS - 1))
1452                .unwrap()
1453                .is_negative()
1454        );
1455    }
1456
1457    #[test]
1458    fn shifts_match_arithmetic_and_negative_count_semantics() {
1459        let value = bigint("0x5a4653ca673768565b41f775");
1460        assert_eq!(
1461            value.shl(&JsBigInt::from(78)).unwrap().to_string(),
1462            "8443945299673273647701379149826607537748959488376832"
1463        );
1464        assert_eq!(
1465            value.shr(&JsBigInt::from(78)).unwrap(),
1466            JsBigInt::from(92441)
1467        );
1468        assert_eq!(
1469            value.neg().unwrap().shr(&JsBigInt::from(78)).unwrap(),
1470            JsBigInt::from(-92442)
1471        );
1472        assert_eq!(
1473            JsBigInt::from(8).shl(&JsBigInt::from(-1)).unwrap(),
1474            JsBigInt::from(4)
1475        );
1476        assert_eq!(
1477            JsBigInt::from(8).shr(&JsBigInt::from(-2)).unwrap(),
1478            JsBigInt::from(32)
1479        );
1480    }
1481
1482    #[test]
1483    fn excessive_left_shift_is_an_explicit_error_but_right_shift_saturates() {
1484        let huge = JsBigInt::from(MAX_BIGINT_BITS + 1);
1485        assert_eq!(JsBigInt::one().shl(&huge), Err(BigIntError::ShiftTooLarge));
1486        assert_eq!(JsBigInt::zero().shl(&huge).unwrap(), JsBigInt::zero());
1487        assert_eq!(JsBigInt::one().shr(&huge).unwrap(), JsBigInt::zero());
1488        assert_eq!(JsBigInt::from(-1).shr(&huge).unwrap(), JsBigInt::from(-1));
1489        assert_eq!(
1490            JsBigInt::one().shr(&huge.neg().unwrap()),
1491            Err(BigIntError::ShiftTooLarge)
1492        );
1493    }
1494
1495    #[test]
1496    fn shift_preserves_quickjs_unguarded_sign_limb_extension() {
1497        let boundary_count = JsBigInt::from(MAX_BIGINT_BITS - 1);
1498        let boundary = JsBigInt::one().shl(&boundary_count).unwrap();
1499        assert_eq!(boundary.signed_limb_len(), MAX_BIGINT_LIMBS + 1);
1500        assert_eq!(boundary.shr(&boundary_count).unwrap(), JsBigInt::one());
1501
1502        assert_eq!(
1503            boundary.shl(&JsBigInt::zero()),
1504            Err(BigIntError::ShiftTooLarge)
1505        );
1506        assert_eq!(
1507            boundary.shr(&JsBigInt::zero()),
1508            Err(BigIntError::ShiftTooLarge)
1509        );
1510        assert_eq!(
1511            boundary.bit_and(&JsBigInt::from(-1)),
1512            Err(BigIntError::AllocationTooLarge)
1513        );
1514        assert_eq!(boundary.bit_not(), Err(BigIntError::AllocationTooLarge));
1515        assert_eq!(
1516            boundary.add(&JsBigInt::zero()),
1517            Err(BigIntError::AllocationTooLarge)
1518        );
1519        assert_eq!(boundary.neg(), Err(BigIntError::AllocationTooLarge));
1520
1521        assert_eq!(
1522            JsBigInt::one().shl(&JsBigInt::from(MAX_BIGINT_BITS)),
1523            Err(BigIntError::ShiftTooLarge)
1524        );
1525        assert_eq!(
1526            JsBigInt::zero().shl(&JsBigInt::from(u64::MAX)).unwrap(),
1527            JsBigInt::zero()
1528        );
1529
1530        let near_boundary = JsBigInt::one()
1531            .shl(&JsBigInt::from(MAX_BIGINT_BITS - 2))
1532            .unwrap();
1533        assert_eq!(near_boundary.add(&JsBigInt::zero()).unwrap(), near_boundary);
1534        assert_eq!(
1535            near_boundary.bit_and(&JsBigInt::from(-1)).unwrap(),
1536            near_boundary
1537        );
1538        assert_eq!(
1539            near_boundary.mul(&JsBigInt::zero()),
1540            Err(BigIntError::AllocationTooLarge)
1541        );
1542        assert_eq!(
1543            near_boundary.div(&JsBigInt::one()),
1544            Err(BigIntError::AllocationTooLarge)
1545        );
1546        assert_eq!(
1547            near_boundary.rem(&JsBigInt::one()),
1548            Err(BigIntError::AllocationTooLarge)
1549        );
1550        assert_eq!(near_boundary.pow(&JsBigInt::one()).unwrap(), near_boundary);
1551        let added_boundary = near_boundary.add(&near_boundary).unwrap();
1552        assert_eq!(added_boundary, boundary);
1553
1554        let negative_boundary = JsBigInt::from(-1)
1555            .shl(&JsBigInt::from(MAX_BIGINT_BITS - 1))
1556            .unwrap();
1557        assert_eq!(negative_boundary.neg().unwrap(), boundary);
1558        assert_eq!(
1559            boundary.pow(&JsBigInt::one()),
1560            Err(BigIntError::AllocationTooLarge)
1561        );
1562        assert_eq!(boundary.pow(&JsBigInt::zero()).unwrap(), JsBigInt::one());
1563    }
1564
1565    #[test]
1566    fn bitwise_operations_use_infinite_twos_complement() {
1567        assert_eq!(
1568            JsBigInt::from(0x5a65_3ca6).bit_not().unwrap(),
1569            JsBigInt::from(-1_516_584_103)
1570        );
1571        let left = JsBigInt::from(0x5a46_3ca6);
1572        let right = JsBigInt::from(0x6737_6856);
1573        assert_eq!(left.bit_or(&right).unwrap(), JsBigInt::from(2_138_537_206));
1574        assert_eq!(left.bit_and(&right).unwrap(), JsBigInt::from(1_107_699_718));
1575        assert_eq!(left.bit_xor(&right).unwrap(), JsBigInt::from(1_030_837_488));
1576
1577        let huge = bigint("1208925819614629174706176"); // 2^80
1578        assert_eq!(
1579            huge.bit_or(&JsBigInt::from(-1)).unwrap(),
1580            JsBigInt::from(-1)
1581        );
1582        assert_eq!(huge.bit_and(&JsBigInt::from(-1)).unwrap(), huge);
1583    }
1584
1585    #[test]
1586    fn comparison_is_numeric_across_storage_classes() {
1587        let short = JsBigInt::from(i64::MAX);
1588        let heap = short.add(&JsBigInt::one()).unwrap();
1589        assert!(short < heap);
1590        assert!(bigint("-9223372036854775809") < JsBigInt::from(i64::MIN));
1591        assert_eq!(
1592            bigint("123456789012345678901234567890"),
1593            bigint("123456789012345678901234567890")
1594        );
1595    }
1596
1597    #[test]
1598    fn conversion_to_f64_rounds_ties_to_even_and_overflows_like_quickjs() {
1599        assert_eq!(JsBigInt::from(42).to_f64(), 42.0);
1600        assert_eq!(bigint("9007199254740993").to_f64(), 9_007_199_254_740_992.0);
1601        assert_eq!(bigint("9007199254740995").to_f64(), 9_007_199_254_740_996.0);
1602        assert_eq!(
1603            bigint("-9007199254740995").to_f64(),
1604            -9_007_199_254_740_996.0
1605        );
1606
1607        let two_to_1023 = JsBigInt::one().shl(&JsBigInt::from(1023)).unwrap();
1608        assert_eq!(two_to_1023.to_f64(), 2.0_f64.powi(1023));
1609        let two_to_1024 = two_to_1023.shl(&JsBigInt::one()).unwrap();
1610        assert_eq!(two_to_1024.to_f64(), f64::INFINITY);
1611        assert_eq!(two_to_1024.neg().unwrap().to_f64(), f64::NEG_INFINITY);
1612    }
1613
1614    #[test]
1615    fn as_int_n_truncates_and_sign_extends() {
1616        assert_eq!(JsBigInt::from(257).as_int_n(8).unwrap(), JsBigInt::one());
1617        assert_eq!(JsBigInt::from(255).as_int_n(8).unwrap(), JsBigInt::from(-1));
1618        assert_eq!(
1619            JsBigInt::from(-129).as_int_n(8).unwrap(),
1620            JsBigInt::from(127)
1621        );
1622        assert_eq!(
1623            bigint("18446744073709551616").as_int_n(65).unwrap(),
1624            bigint("-18446744073709551616")
1625        );
1626        assert_eq!(
1627            bigint("36893488147419103231").as_int_n(65).unwrap(),
1628            JsBigInt::from(-1)
1629        );
1630        assert_eq!(
1631            bigint("999999999999999999999").as_int_n(0).unwrap(),
1632            JsBigInt::zero()
1633        );
1634    }
1635
1636    #[test]
1637    fn quickjs_2026_as_uint_n_preserves_signed_limb_quirks() {
1638        // These results were verified against qjs 2026-06-04. They differ from
1639        // ECMA-262/Node, but observable baseline parity takes precedence here.
1640        assert_eq!(
1641            JsBigInt::from(-1).as_uint_n(64).unwrap(),
1642            JsBigInt::from(-1)
1643        );
1644        assert_eq!(
1645            JsBigInt::from(-1).as_uint_n(128).unwrap(),
1646            JsBigInt::from(-1)
1647        );
1648        assert_eq!(
1649            bigint("9223372036854775808").as_uint_n(64).unwrap(),
1650            JsBigInt::from(i64::MIN)
1651        );
1652        assert_eq!(
1653            bigint("18446744073709551615").as_uint_n(64).unwrap(),
1654            JsBigInt::from(-1)
1655        );
1656        assert_eq!(
1657            bigint("-18446744073709551616").as_uint_n(128).unwrap(),
1658            bigint("-18446744073709551616")
1659        );
1660        // Non-limb-aligned truncation clears the spare sign bits and is
1661        // positive, matching js_bigint_asUintN's logical shift path.
1662        assert_eq!(
1663            bigint("-18446744073709551616").as_uint_n(65).unwrap(),
1664            bigint("18446744073709551616")
1665        );
1666    }
1667
1668    #[test]
1669    fn as_n_preserves_the_extended_limb_allocation_gap() {
1670        let extended = JsBigInt::one()
1671            .shl(&JsBigInt::from(MAX_BIGINT_BITS - 1))
1672            .unwrap();
1673        assert_eq!(extended.signed_limb_len(), MAX_BIGINT_LIMBS + 1);
1674
1675        assert_eq!(
1676            extended.as_uint_n(MAX_BIGINT_BITS - 1).unwrap(),
1677            JsBigInt::zero()
1678        );
1679        assert_eq!(
1680            extended.as_int_n(MAX_BIGINT_BITS - 1).unwrap(),
1681            JsBigInt::zero()
1682        );
1683
1684        let at_nominal_limit = extended.as_uint_n(MAX_BIGINT_BITS).unwrap();
1685        let exact_negative = JsBigInt::normalize(-extended.to_bigint());
1686        assert_eq!(at_nominal_limit, exact_negative);
1687        assert_eq!(extended.as_int_n(MAX_BIGINT_BITS).unwrap(), exact_negative);
1688
1689        for bits in MAX_BIGINT_BITS + 1..MAX_BIGINT_BITS + BIGINT_LIMB_BITS {
1690            assert_eq!(
1691                extended.as_uint_n(bits),
1692                Err(BigIntError::AllocationTooLarge)
1693            );
1694            assert_eq!(
1695                extended.as_int_n(bits),
1696                Err(BigIntError::AllocationTooLarge)
1697            );
1698        }
1699        assert_eq!(
1700            extended
1701                .as_uint_n(MAX_BIGINT_BITS + BIGINT_LIMB_BITS)
1702                .unwrap(),
1703            extended
1704        );
1705        assert_eq!(
1706            extended
1707                .as_int_n(MAX_BIGINT_BITS + BIGINT_LIMB_BITS + 1)
1708                .unwrap(),
1709            extended
1710        );
1711        assert_eq!(extended.as_uint_n(u64::MAX).unwrap(), extended);
1712    }
1713
1714    #[test]
1715    fn integral_f64_conversion_preserves_the_exact_binary_value() {
1716        assert_eq!(JsBigInt::from_integral_f64(-0.0).unwrap(), JsBigInt::zero());
1717        assert_eq!(
1718            JsBigInt::from_integral_f64(9_007_199_254_740_991.0).unwrap(),
1719            bigint("9007199254740991")
1720        );
1721        assert_eq!(
1722            JsBigInt::from_integral_f64(9_007_199_254_740_992.0).unwrap(),
1723            bigint("9007199254740992")
1724        );
1725        assert_eq!(
1726            JsBigInt::from_integral_f64(9_007_199_254_740_994.0).unwrap(),
1727            bigint("9007199254740994")
1728        );
1729
1730        let max_finite = JsBigInt::one()
1731            .shl(&JsBigInt::from(1024))
1732            .unwrap()
1733            .sub(&JsBigInt::one().shl(&JsBigInt::from(971)).unwrap())
1734            .unwrap();
1735        assert_eq!(JsBigInt::from_integral_f64(f64::MAX).unwrap(), max_finite);
1736        assert_eq!(
1737            JsBigInt::from_integral_f64(f64::MIN).unwrap(),
1738            max_finite.neg().unwrap()
1739        );
1740
1741        assert!(JsBigInt::from_integral_f64(1.5).is_none());
1742        assert!(JsBigInt::from_integral_f64(f64::NAN).is_none());
1743        assert!(JsBigInt::from_integral_f64(f64::INFINITY).is_none());
1744        assert!(JsBigInt::from_integral_f64(f64::NEG_INFINITY).is_none());
1745    }
1746
1747    #[test]
1748    fn arbitrary_precision_is_not_capped_at_i128() {
1749        let value = JsBigInt::from(3).pow(&JsBigInt::from(1000)).unwrap();
1750        assert!(value.magnitude_bits() > 128);
1751        let text = value.to_string();
1752        assert_eq!(text.len(), 478);
1753        assert!(text.starts_with("1322070819480806636890455259752144365965"));
1754        assert_eq!(JsBigInt::parse_js_string(&text).unwrap(), value);
1755    }
1756}