quickjs-oxide-core 0.4.0

The new quickjs-oxide core: 8-byte values, typed arenas and a mark-sweep heap
Documentation
//! `BigInt`: the function (callable, not constructible), `asIntN`, `asUintN`,
//! and `BigInt.prototype`'s `toString(radix)`, `toLocaleString` and `valueOf`.

use super::{Native, install_constructor, link_constructor, methods, string_text, to_string_tag};
use crate::heap::HeapError;
use crate::heap::object::ObjectKind;
use crate::runtime::Intrinsic;
use crate::value::{Kind, Value};
use crate::vm::bigint::{Big, BigError, qjs};
use crate::vm::native::CallSite;
use crate::vm::object as internal;
use crate::vm::{Cx, Throw, error, ops};

pub(super) fn install(cx: &mut Cx<'_>) -> Result<(), HeapError> {
    let object_prototype = cx.realm.intrinsic(Intrinsic::ObjectPrototype);
    let prototype = super::plain_object(cx, Some(object_prototype), 4)?;
    cx.set_intrinsic(Intrinsic::BigIntPrototype, prototype);
    let bigint = install_constructor(cx, Native::BigIntConstructor)?;
    cx.set_intrinsic(Intrinsic::BigInt, bigint);
    methods(cx, bigint, &[Native::BigIntAsUintN, Native::BigIntAsIntN])?;
    methods(
        cx,
        prototype,
        &[
            Native::BigIntToString,
            Native::BigIntToLocaleString,
            Native::BigIntValueOf,
        ],
    )?;
    to_string_tag(cx, prototype, "BigInt")?;
    link_constructor(cx, bigint, prototype)
}

fn alloc(cx: &mut Cx<'_>, value: Result<Big, BigError>) -> Result<Value, Throw> {
    match value {
        Ok(value) => cx
            .heap
            .alloc_bigint(value.negative, &value.limbs)
            .map(Value::bigint)
            .map_err(|error| error::heap_error(cx, error)),
        Err(_) => Err(too_large(cx)),
    }
}

#[cold]
#[inline(never)]
fn too_large(cx: &mut Cx<'_>) -> Throw {
    error::range_error(cx, "BigInt is too large to allocate")
}

/// `ToBigInt(value)`.
pub(crate) fn to_bigint(cx: &mut Cx<'_>, value: Value) -> Result<Value, Throw> {
    convert(cx, value, false)
}

/// `ToBigInt(value)`; `constructor` selects the `BigInt(value)` messages
/// (QuickJS's `JS_ToBigIntCtorFree`) over `ToBigInt`'s.
fn convert(cx: &mut Cx<'_>, value: Value, constructor: bool) -> Result<Value, Throw> {
    let primitive = ops::to_primitive(cx, value, ops::Hint::Number)?;
    let failure = if constructor {
        "cannot convert to BigInt"
    } else {
        "cannot convert to bigint"
    };
    match primitive.kind() {
        Kind::BigInt => Ok(primitive),
        Kind::Bool => alloc(cx, Ok(Big::from_u64(u64::from(primitive == Value::TRUE)))),
        Kind::String => {
            let text = ops::string_text(cx.heap, primitive);
            match Big::parse_checked(&text) {
                Some(value) => alloc(cx, value),
                None => Err(error::throw(
                    cx,
                    crate::ErrorKind::Syntax,
                    "invalid bigint literal",
                )),
            }
        }
        Kind::Undefined | Kind::Null | Kind::Empty | Kind::Number | Kind::Int32 | Kind::Symbol => {
            Err(error::type_error(cx, failure))
        }
        Kind::Object => unreachable!("ToPrimitive returned an object"),
    }
}

/// `BigInt(value)`.
pub(super) fn constructor(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
    if site.construct {
        return Err(error::not_a_constructor(cx, site.new_target));
    }
    let value = site.arg(cx, 0);
    let primitive = ops::to_primitive(cx, value, ops::Hint::Number)?;
    if let Some(number) = primitive.as_f64() {
        if !number.is_finite() {
            return Err(error::range_error(
                cx,
                "cannot convert NaN or Infinity to BigInt",
            ));
        }
        if number.trunc() != number {
            return Err(error::range_error(
                cx,
                "cannot convert to BigInt: not an integer",
            ));
        }
        let big = Big::from_f64(number).expect("an integer");
        return alloc(cx, Ok(big));
    }
    convert(cx, primitive, true)
}

/// `thisBigIntValue`.
fn this_bigint(cx: &mut Cx<'_>, site: &CallSite) -> Result<Value, Throw> {
    let this = site.this(cx);
    if this.as_bigint().is_some() {
        return Ok(this);
    }
    if let Some(object) = this.as_object()
        && cx.heap.object_kind(object) == ObjectKind::BigIntWrapper
    {
        return Ok(cx.heap.internal_slot(object, 0));
    }
    Err(error::type_error(cx, "not a BigInt"))
}

pub(super) fn value_of(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
    this_bigint(cx, &site)
}

/// `BigInt.prototype.toLocaleString` (21.2.3.2): without ECMA-402, the
/// value's `toString`.
pub(super) fn to_locale_string(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
    let value = this_bigint(cx, &site)?;
    ops::to_string(cx, value)
}

pub(super) fn to_string(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
    let value = this_bigint(cx, &site)?;
    let radix = site.arg(cx, 0);
    let radix = if radix.is_undefined() {
        10.0
    } else {
        internal::to_integer_or_infinity(cx, radix)?
    };
    if !(2.0..=36.0).contains(&radix) {
        return Err(error::range_error(cx, "radix must be between 2 and 36"));
    }
    let big = ops::big(cx.heap, value);
    if qjs::to_string_check(&big, radix as u32).is_err() {
        return Err(too_large(cx));
    }
    string_text(cx, &radix_text(&big, radix as u32))
}

/// The digits of `value` in `radix`.
pub(crate) fn radix_text(value: &Big, radix: u32) -> String {
    if radix == 10 {
        return value.to_string_radix10();
    }
    if value.is_zero() {
        return "0".into();
    }
    const DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
    let mut out = Vec::new();
    if radix.is_power_of_two() {
        // Read the digits off the bits, low first.
        let width = radix.trailing_zeros() as u64;
        let limbs = &value.limbs;
        let bits = limbs.len() as u64 * 64 - u64::from(limbs.last().unwrap().leading_zeros());
        let mut position = 0;
        while position < bits {
            let (index, offset) = ((position / 64) as usize, position % 64);
            let mut digit = limbs[index] >> offset;
            if offset + width > 64 && index + 1 < limbs.len() {
                digit |= limbs[index + 1] << (64 - offset);
            }
            out.push(DIGITS[(digit & u64::from(radix - 1)) as usize]);
            position += width;
        }
        if value.negative {
            out.push(b'-');
        }
        out.reverse();
        return String::from_utf8(out).expect("ASCII digits");
    }
    let mut rest = Big::new(false, value.limbs.clone());
    let divisor = Big::from_u64(u64::from(radix));
    while !rest.is_zero() {
        let (quotient, remainder) = rest.div_rem(&divisor).expect("a non-zero divisor");
        out.push(DIGITS[remainder.limbs.first().copied().unwrap_or(0) as usize]);
        rest = quotient;
    }
    if value.negative {
        out.push(b'-');
    }
    out.reverse();
    String::from_utf8(out).expect("ASCII digits")
}

/// `asIntN` and `asUintN`, as QuickJS computes them ([`qjs::as_n`]).
fn as_n(cx: &mut Cx<'_>, site: CallSite, signed: bool) -> Result<Value, Throw> {
    let bits = site.arg(cx, 0);
    let bits = to_index(cx, bits)?;
    let value = site.arg(cx, 1);
    let value = to_bigint(cx, value)?;
    cx.heap.roots().push(value);
    let big = ops::big(cx.heap, value);
    match qjs::as_n(&big, bits, signed) {
        Ok(result) if result == big => Ok(value),
        result => alloc(cx, result),
    }
}

pub(super) fn as_int_n(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
    as_n(cx, site, true)
}

pub(super) fn as_uint_n(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
    as_n(cx, site, false)
}

/// `ToIndex(value)`.
pub(crate) fn to_index(cx: &mut Cx<'_>, value: Value) -> Result<u64, Throw> {
    if value.is_undefined() {
        return Ok(0);
    }
    let integer = internal::to_integer_or_infinity(cx, value)?;
    if !(0.0..=9_007_199_254_740_991.0).contains(&integer) {
        return Err(error::range_error(cx, "invalid array index"));
    }
    Ok(integer as u64)
}