Skip to content

Commit

Permalink
Add TryFrom<JsValue> for [iu](64|128) (#3058)
Browse files Browse the repository at this point in the history
Adds a way to convert `JsValue` or a `BigInt` to `i64`/`u64`/`i128`/`u128` with type and range checks, returning the original `JsValue` otherwise.

This could be optimised a little bit further via more intrinsics, but it's good enough for the initial implementation, so leaving any optimisations for the future.

Fixes #2350.
  • Loading branch information
RReverser committed Sep 4, 2022
1 parent 8c7633a commit cb94b43
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 2 deletions.
7 changes: 7 additions & 0 deletions crates/cli-support/src/intrinsic.rs
Expand Up @@ -80,6 +80,10 @@ fn opt_f64() -> Descriptor {
Descriptor::Option(Box::new(Descriptor::F64))
}

fn opt_i64() -> Descriptor {
Descriptor::Option(Box::new(Descriptor::I64))
}

intrinsics! {
pub enum Intrinsic {
#[symbol = "__wbindgen_jsval_eq"]
Expand Down Expand Up @@ -208,6 +212,9 @@ intrinsics! {
#[symbol = "__wbindgen_bigint_from_u128"]
#[signature = fn(U64, U64) -> Externref]
BigIntFromU128,
#[symbol = "__wbindgen_bigint_get_as_i64"]
#[signature = fn(ref_externref()) -> opt_i64()]
BigIntGetAsI64,
#[symbol = "__wbindgen_string_new"]
#[signature = fn(ref_string()) -> Externref]
StringNew,
Expand Down
6 changes: 6 additions & 0 deletions crates/cli-support/src/js/mod.rs
Expand Up @@ -3384,6 +3384,12 @@ impl<'a> Context<'a> {
format!("typeof(v) === 'boolean' ? (v ? 1 : 0) : 2")
}

Intrinsic::BigIntGetAsI64 => {
assert_eq!(args.len(), 1);
prelude.push_str(&format!("const v = {};\n", args[0]));
format!("typeof(v) === 'bigint' ? v : undefined")
}

Intrinsic::Throw => {
assert_eq!(args.len(), 1);
format!("throw new Error({})", args[0])
Expand Down
13 changes: 11 additions & 2 deletions crates/js-sys/src/lib.rs
Expand Up @@ -20,7 +20,7 @@

use core::ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Not, Rem, Shl, Shr, Sub};
use std::cmp::Ordering;
use std::convert::{self, Infallible};
use std::convert::{self, Infallible, TryFrom};
use std::f64;
use std::fmt;
use std::iter::{self, Product, Sum};
Expand Down Expand Up @@ -1046,7 +1046,7 @@ macro_rules! bigint_from {
impl PartialEq<$x> for BigInt {
#[inline]
fn eq(&self, other: &$x) -> bool {
JsValue::from(self) == BigInt::from(*other).unchecked_into::<JsValue>()
JsValue::from(self) == JsValue::from(BigInt::from(*other))
}
}
)*)
Expand All @@ -1068,6 +1068,15 @@ macro_rules! bigint_from_big {
self == &BigInt::from(*other)
}
}

impl TryFrom<BigInt> for $x {
type Error = BigInt;

#[inline]
fn try_from(x: BigInt) -> Result<Self, BigInt> {
Self::try_from(JsValue::from(x)).map_err(JsCast::unchecked_into)
}
}
)*)
}
bigint_from_big!(i64 u64 i128 u128);
Expand Down
57 changes: 57 additions & 0 deletions src/lib.rs
Expand Up @@ -886,6 +886,62 @@ macro_rules! big_numbers {
)*)
}

fn bigint_get_as_i64(v: &JsValue) -> Option<i64> {
unsafe { Option::from_abi(__wbindgen_bigint_get_as_i64(v.idx)) }
}

macro_rules! try_from_for_num64 {
($ty:ty) => {
impl TryFrom<JsValue> for $ty {
type Error = JsValue;

#[inline]
fn try_from(v: JsValue) -> Result<Self, JsValue> {
bigint_get_as_i64(&v)
// Reinterpret bits; ABI-wise this is safe to do and allows us to avoid
// having separate intrinsics per signed/unsigned types.
.map(|as_i64| as_i64 as Self)
// Double-check that we didn't truncate the bigint to 64 bits.
.filter(|as_self| v == *as_self)
// Not a bigint or not in range.
.ok_or(v)
}
}
};
}

try_from_for_num64!(i64);
try_from_for_num64!(u64);

macro_rules! try_from_for_num128 {
($ty:ty, $hi_ty:ty) => {
impl TryFrom<JsValue> for $ty {
type Error = JsValue;

#[inline]
fn try_from(v: JsValue) -> Result<Self, JsValue> {
// Truncate the bigint to 64 bits, this will give us the lower part.
let lo = match bigint_get_as_i64(&v) {
// The lower part must be interpreted as unsigned in both i128 and u128.
Some(lo) => lo as u64,
// Not a bigint.
None => return Err(v),
};
// Now we know it's a bigint, so we can safely use `>> 64n` without
// worrying about a JS exception on type mismatch.
let hi = v >> JsValue::from(64_u64);
// The high part is the one we want checked against a 64-bit range.
// If it fits, then our original number is in the 128-bit range.
let hi = <$hi_ty>::try_from(hi)?;
Ok(Self::from(hi) << 64 | Self::from(lo))
}
}
};
}

try_from_for_num128!(i128, i64);
try_from_for_num128!(u128, u64);

big_numbers! {
|n|,
i64 = __wbindgen_bigint_from_i64(n),
Expand Down Expand Up @@ -980,6 +1036,7 @@ externs! {
fn __wbindgen_number_get(idx: u32) -> WasmOption<f64>;
fn __wbindgen_boolean_get(idx: u32) -> u32;
fn __wbindgen_string_get(idx: u32) -> WasmSlice;
fn __wbindgen_bigint_get_as_i64(idx: u32) -> WasmOption<i64>;

fn __wbindgen_debug_string(ret: *mut [usize; 2], idx: u32) -> ();

Expand Down
29 changes: 29 additions & 0 deletions tests/wasm/bigint.rs
Expand Up @@ -87,3 +87,32 @@ pub fn u64_slice(a: &[u64]) -> Vec<u64> {
fn works() {
js_works();
}

mod try_from_works {
use super::*;
use crate::JsValue;
use core::convert::TryFrom;

macro_rules! test_type_boundaries {
($($ty:ident)*) => {
$(
#[wasm_bindgen_test]
fn $ty() {
// Not a bigint.
assert!($ty::try_from(JsValue::NULL).is_err());
assert!($ty::try_from(JsValue::from_f64(0.0)).is_err());
// Within range.
assert_eq!($ty::try_from(JsValue::from($ty::MIN)), Ok($ty::MIN));
// Too small.
assert!($ty::try_from(JsValue::from($ty::MIN) - JsValue::from(1_i64)).is_err());
// Within range.
assert_eq!($ty::try_from(JsValue::from($ty::MAX)), Ok($ty::MAX));
// Too large.
assert!($ty::try_from(JsValue::from($ty::MAX) + JsValue::from(1_i64)).is_err());
}
)*
};
}

test_type_boundaries!(i64 u64 i128 u128);
}

0 comments on commit cb94b43

Please sign in to comment.